diff options
| author | Andreas Grois <andi@grois.info> | 2022-10-10 21:30:02 +0200 |
|---|---|---|
| committer | Andreas Grois <andi@grois.info> | 2022-10-10 21:37:15 +0200 |
| commit | e4ad766315879e1ff05bb111229f073f8f0ed68e (patch) | |
| tree | 4b043ff47c78b2c00c80c94ebda622c32c8b6d3d | |
PassFish: Initial Commit
Well, that's a lie. But nobody needs to see all the iterations I decided
to sweep under the rug.
That said, I think the repo is, while not clean, clean enough now, to
not be embarrassed by uploading it to github.
61 files changed, 7644 insertions, 0 deletions
diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..bb7b593 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Use target-compatible line endings as the safe default for cross compilation. +* text=auto eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2c2ff34 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +/rust/target +/rust_macro/target +/rust_testhelper/target +*~ +*.swp +CMakeLists.txt.user +passwordmaker-javascript-2.5.html +*.pdf +notes diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..9d4d67f --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,173 @@ +project(harbour-passfish) +cmake_minimum_required(VERSION 3.5) +cmake_policy(SET CMP0046 NEW) +cmake_policy(SET CMP0071 NEW) + +LIST(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") + +find_package (Qt5 COMPONENTS Core Network Qml Gui Quick REQUIRED) +find_package(RustQtBindingGenerator REQUIRED) +find_package(Cargo REQUIRED) +find_package(Rust REQUIRED) + +set(CMAKE_THREAD_PREFER_PTHREAD TRUE) +find_package(Threads REQUIRED) + +include(FindPkgConfig) +pkg_search_module(SAILFISH sailfishapp REQUIRED) + +add_definitions(${SAILFISH_CFLAGS}) +link_libraries(${SAILFISH_LDFLAGS}) + +set(CMAKE_AUTOMOC ON) +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +if(NOT DEFINED RUST_TARGET_TRIPLET) + message( FATAL_ERROR "Auto-Detection of Rust target triplet is currently not implemented. Please set RUST_TARGET_TRIPLET on the cmake command line." ) +endif() +message(STATUS "Rust Target=${RUST_TARGET_TRIPLET}") + +string(TOUPPER "${CMAKE_BUILD_TYPE}" CMAKE_BUILD_TYPE_UPPER) +if(CMAKE_BUILD_TYPE_UPPER STREQUAL DEBUG) + set(RUST_TARGET_DIR target/${RUST_TARGET_TRIPLET}/debug/) + set(RUST_BUILD_FLAG --target=${RUST_TARGET_TRIPLET}) +else() + set(RUST_TARGET_DIR target/${RUST_TARGET_TRIPLET}/release/) + set(RUST_BUILD_FLAG --release --target=${RUST_TARGET_TRIPLET}) +endif() +SET(RUST_DIR "${CMAKE_CURRENT_SOURCE_DIR}/rust") +SET(RUST_LIB "${RUST_DIR}/${RUST_TARGET_DIR}/libpassfish.so") + +#Must set RPATH to custom folder where Sailfish wants us to put our shared lib +set(CMAKE_INSTALL_RPATH "/usr/share/harbour-passfish/lib") +set(CMAKE_SKIP_RPATH false) + +# generate c++ and rust code from bindings.json +add_custom_command( + OUTPUT "${RUST_DIR}/src/interface.rs" + "${CMAKE_CURRENT_SOURCE_DIR}/src/Bindings.h" + "${CMAKE_CURRENT_SOURCE_DIR}/src/Bindings.cpp" + COMMAND "${RustQtBindingGenerator_EXECUTABLE}" #--overwrite-implementation + "${CMAKE_CURRENT_SOURCE_DIR}/bindings.json" + DEPENDS bindings.json +) + +# compile the rust code into a static library +FILE(GLOB_RECURSE RustSources "rust/src/*.rs") +FILE(GLOB_RECURSE RustMacroSources "rust_macro/*.rs") +FILE(GLOB_RECURSE RustTesthelperSources "rust_testhelper/*.rs") +add_custom_command( + OUTPUT "${RUST_LIB}" + COMMAND ${Cargo_EXECUTABLE} build ${RUST_BUILD_FLAG} + DEPENDS rust/Cargo.toml + ${RustSources} + rust/src/interface.rs + rust_macro/Cargo.toml + ${RustMacroSources} + rust_testhelper/Cargo.toml + ${RustTesthelperSources} + WORKING_DIRECTORY "${RUST_DIR}" +) +add_custom_target(rust_target DEPENDS "${RUST_LIB}") + +add_executable( + harbour-passfish + src/PassFish.cpp + src/Bindings.cpp + src/pwm_qhash.cpp + src/GraphemeCountValidator.cpp +) +add_dependencies(harbour-passfish rust_target) +target_compile_definitions(harbour-passfish PRIVATE + $<$<OR:$<CONFIG:Debug>,$<CONFIG:RelWithDebInfo>>:QT_QML_DEBUG> +) +target_include_directories(harbour-passfish PRIVATE + $<BUILD_INTERFACE: + ${SAILFISH_INCLUDE_DIRS} +>) +target_link_libraries(harbour-passfish + Qt5::Quick + ${RUST_LIB} + ${CMAKE_THREAD_LIBS_INIT} + ${CMAKE_DL_LIBS} + ${SAILFISH_LDFLAGS} +) + +install(TARGETS harbour-passfish + RUNTIME DESTINATION bin +) +install(FILES ${RUST_LIB} + DESTINATION share/harbour-passfish/lib +) +install(DIRECTORY qml + DESTINATION share/harbour-passfish +) +install(DIRECTORY translations + DESTINATION share/harbour-passfish + FILES_MATCHING PATTERN "*.qm" +) +install(FILES harbour-passfish.desktop + DESTINATION share/applications +) +install(FILES icons/86x86/harbour-passfish.png + DESTINATION share/icons/hicolor/86x86/apps +) +install(FILES icons/108x108/harbour-passfish.png + DESTINATION share/icons/hicolor/108x108/apps +) +install(FILES icons/128x128/harbour-passfish.png + DESTINATION share/icons/hicolor/128x128/apps +) +install(FILES icons/172x172/harbour-passfish.png + DESTINATION share/icons/hicolor/172x172/apps +) + +# Get the other files reachable from the project tree in Qt Creator +FILE(GLOB TsFiles "translations/*.ts") +add_custom_target(distfiles + SOURCES + harbour-passfish.desktop + qml/PassFish.qml + qml/cover/CoverPage.qml + qml/pages/MainPage.qml + qml/pages/AboutPage.qml + qml/pages/ProfilesPage.qml + qml/pages/ProfileEditor.qml + qml/pages/SettingsEditor.qml + qml/components/NoticeOptional.qml + qml/helpers/NoticeLoadable.qml + qml/helpers/NotificationLoadable.qml + rpm/harbour-passfish.changes.in + rpm/harbour-passfish.changes.run.in + rpm/harbour-passfish.spec + rpm/harbour-passfish.yaml + ${TsFiles}) +FILE(GLOB AddCMakeFiles "cmake/*.cmake") +add_custom_target(cmake_helpers + SOURCES + PreLoad.cmake + ${AddCMakeFiles}) + +add_custom_target(rust_bindings + SOURCES + bindings.json + rust/Cargo.toml + ${RustSources} + rust_macro/Cargo.toml + ${RustMacroSources} + rust_testhelper/Cargo.toml + ${RustTesthelperSources}) + +# Tell Qt Creator where the application executable(s) would be located on the +# device. +# +# It is not necessary to list other deployables than executables (runtime +# targets) here. The deployment process of Sailfish OS projects is opaque to +# Qt Creator and the information contained in QtCreatorDeployment.txt is only +# used to locate the executable associated with the active run configuration +# on the device in order to run it. +# +# Search the Qt Creator Manual to learn about the QtCreatorDeployment.txt file +# format. +file(WRITE "${CMAKE_BINARY_DIR}/QtCreatorDeployment.txt" + "${CMAKE_INSTALL_PREFIX}\n${CMAKE_BINARY_DIR}/harbour-passfish:bin\n") @@ -0,0 +1,604 @@ +GNU GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright © 2007 Free Software Foundation, Inc. <http://fsf.org/> + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +Preamble + +The GNU General Public License is a free, copyleft license for software and +other kinds of works. + +The licenses for most software and other practical works are designed to take +away your freedom to share and change the works. By contrast, the GNU General +Public License is intended to guarantee your freedom to share and change all +versions of a program--to make sure it remains free software for all its users. +We, the Free Software Foundation, use the GNU General Public License for most +of our software; it applies also to any other work released this way by its +authors. You can apply it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our +General Public Licenses are designed to make sure that you have the freedom +to distribute copies of free software (and charge for them if you wish), that +you receive source code or can get it if you want it, that you can change +the software or use pieces of it in new free programs, and that you know you +can do these things. + +To protect your rights, we need to prevent others from denying you these rights +or asking you to surrender the rights. Therefore, you have certain responsibilities +if you distribute copies of the software, or if you modify it: responsibilities +to respect the freedom of others. + +For example, if you distribute copies of such a program, whether gratis or +for a fee, you must pass on to the recipients the same freedoms that you received. +You must make sure that they, too, receive or can get the source code. And +you must show them these terms so they know their rights. + +Developers that use the GNU GPL protect your rights with two steps: (1) assert +copyright on the software, and (2) offer you this License giving you legal +permission to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains that +there is no warranty for this free software. For both users' and authors' +sake, the GPL requires that modified versions be marked as changed, so that +their problems will not be attributed erroneously to authors of previous versions. + +Some devices are designed to deny users access to install or run modified +versions of the software inside them, although the manufacturer can do so. +This is fundamentally incompatible with the aim of protecting users' freedom +to change the software. The systematic pattern of such abuse occurs in the +area of products for individuals to use, which is precisely where it is most +unacceptable. Therefore, we have designed this version of the GPL to prohibit +the practice for those products. If such problems arise substantially in other +domains, we stand ready to extend this provision to those domains in future +versions of the GPL, as needed to protect the freedom of users. + +Finally, every program is threatened constantly by software patents. States +should not allow patents to restrict development and use of software on general-purpose +computers, but in those that do, we wish to avoid the special danger that +patents applied to a free program could make it effectively proprietary. To +prevent this, the GPL assures that patents cannot be used to render the program +non-free. + +The precise terms and conditions for copying, distribution and modification +follow. + +TERMS AND CONDITIONS + +0. Definitions. + +“This License” refers to version 3 of the GNU General Public License. + +“Copyright” also means copyright-like laws that apply to other kinds of works, +such as semiconductor masks. + +“The Program” refers to any copyrightable work licensed under this License. +Each licensee is addressed as “you”. “Licensees” and “recipients” may be individuals +or organizations. + +To “modify” a work means to copy from or adapt all or part of the work in +a fashion requiring copyright permission, other than the making of an exact +copy. The resulting work is called a “modified version” of the earlier work +or a work “based on” the earlier work. + +A “covered work” means either the unmodified Program or a work based on the +Program. + +To “propagate” a work means to do anything with it that, without permission, +would make you directly or secondarily liable for infringement under applicable +copyright law, except executing it on a computer or modifying a private copy. +Propagation includes copying, distribution (with or without modification), +making available to the public, and in some countries other activities as +well. + +To “convey” a work means any kind of propagation that enables other parties +to make or receive copies. Mere interaction with a user through a computer +network, with no transfer of a copy, is not conveying. + +An interactive user interface displays “Appropriate Legal Notices” to the +extent that it includes a convenient and prominently visible feature that +(1) displays an appropriate copyright notice, and (2) tells the user that +there is no warranty for the work (except to the extent that warranties are +provided), that licensees may convey the work under this License, and how +to view a copy of this License. If the interface presents a list of user commands +or options, such as a menu, a prominent item in the list meets this criterion. + +1. Source Code. +The “source code” for a work means the preferred form of the work for making +modifications to it. “Object code” means any non-source form of a work. + +A “Standard Interface” means an interface that either is an official standard +defined by a recognized standards body, or, in the case of interfaces specified +for a particular programming language, one that is widely used among developers +working in that language. + +The “System Libraries” of an executable work include anything, other than +the work as a whole, that (a) is included in the normal form of packaging +a Major Component, but which is not part of that Major Component, and (b) +serves only to enable use of the work with that Major Component, or to implement +a Standard Interface for which an implementation is available to the public +in source code form. A “Major Component”, in this context, means a major essential +component (kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to produce +the work, or an object code interpreter used to run it. + +The “Corresponding Source” for a work in object code form means all the source +code needed to generate, install, and (for an executable work) run the object +code and to modify the work, including scripts to control those activities. +However, it does not include the work's System Libraries, or general-purpose +tools or generally available free programs which are used unmodified in performing +those activities but which are not part of the work. For example, Corresponding +Source includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically linked +subprograms that the work is specifically designed to require, such as by +intimate data communication or control flow between those subprograms and +other parts of the work. + +The Corresponding Source need not include anything that users can regenerate +automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same work. + +2. Basic Permissions. +All rights granted under this License are granted for the term of copyright +on the Program, and are irrevocable provided the stated conditions are met. +This License explicitly affirms your unlimited permission to run the unmodified +Program. The output from running a covered work is covered by this License +only if the output, given its content, constitutes a covered work. This License +acknowledges your rights of fair use or other equivalent, as provided by copyright +law. + +You may make, run and propagate covered works that you do not convey, without +conditions so long as your license otherwise remains in force. You may convey +covered works to others for the sole purpose of having them make modifications +exclusively for you, or provide you with facilities for running those works, +provided that you comply with the terms of this License in conveying all material +for which you do not control copyright. Those thus making or running the covered +works for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of your copyrighted +material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the conditions +stated below. Sublicensing is not allowed; section 10 makes it unnecessary. + +3. Protecting Users' Legal Rights From Anti-Circumvention Law. +No covered work shall be deemed part of an effective technological measure +under any applicable law fulfilling obligations under article 11 of the WIPO +copyright treaty adopted on 20 December 1996, or similar laws prohibiting +or restricting circumvention of such measures. + +When you convey a covered work, you waive any legal power to forbid circumvention +of technological measures to the extent such circumvention is effected by +exercising rights under this License with respect to the covered work, and +you disclaim any intention to limit operation or modification of the work +as a means of enforcing, against the work's users, your or third parties' +legal rights to forbid circumvention of technological measures. + +4. Conveying Verbatim Copies. +You may convey verbatim copies of the Program's source code as you receive +it, in any medium, provided that you conspicuously and appropriately publish +on each copy an appropriate copyright notice; keep intact all notices stating +that this License and any non-permissive terms added in accord with section +7 apply to the code; keep intact all notices of the absence of any warranty; +and give all recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, and you +may offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. +You may convey a work based on the Program, or the modifications to produce +it from the Program, in the form of source code under the terms of section +4, provided that you also meet all of these conditions: + +a) The work must carry prominent notices stating that you modified it, and +giving a relevant date. + +b) The work must carry prominent notices stating that it is released under +this License and any conditions added under section 7. This requirement modifies +the requirement in section 4 to “keep intact all notices”. + +c) You must license the entire work, as a whole, under this License to anyone +who comes into possession of a copy. This License will therefore apply, along +with any applicable section 7 additional terms, to the whole of the work, +and all its parts, regardless of how they are packaged. This License gives +no permission to license the work in any other way, but it does not invalidate +such permission if you have separately received it. + +d) If the work has interactive user interfaces, each must display Appropriate +Legal Notices; however, if the Program has interactive interfaces that do +not display Appropriate Legal Notices, your work need not make them do so. + +A compilation of a covered work with other separate and independent works, +which are not by their nature extensions of the covered work, and which are +not combined with it such as to form a larger program, in or on a volume of +a storage or distribution medium, is called an “aggregate” if the compilation +and its resulting copyright are not used to limit the access or legal rights +of the compilation's users beyond what the individual works permit. Inclusion +of a covered work in an aggregate does not cause this License to apply to +the other parts of the aggregate. + +6. Conveying Non-Source Forms. +You may convey a covered work in object code form under the terms of sections +4 and 5, provided that you also convey the machine-readable Corresponding +Source under the terms of this License, in one of these ways: + +a) Convey the object code in, or embodied in, a physical product (including +a physical distribution medium), accompanied by the Corresponding Source fixed +on a durable physical medium customarily used for software interchange. + +b) Convey the object code in, or embodied in, a physical product (including +a physical distribution medium), accompanied by a written offer, valid for +at least three years and valid for as long as you offer spare parts or customer +support for that product model, to give anyone who possesses the object code +either (1) a copy of the Corresponding Source for all the software in the +product that is covered by this License, on a durable physical medium customarily +used for software interchange, for a price no more than your reasonable cost +of physically performing this conveying of source, or (2) access to copy the +Corresponding Source from a network server at no charge. + +c) Convey individual copies of the object code with a copy of the written +offer to provide the Corresponding Source. This alternative is allowed only +occasionally and noncommercially, and only if you received the object code +with such an offer, in accord with subsection 6b. + +d) Convey the object code by offering access from a designated place (gratis +or for a charge), and offer equivalent access to the Corresponding Source +in the same way through the same place at no further charge. You need not +require recipients to copy the Corresponding Source along with the object +code. If the place to copy the object code is a network server, the Corresponding +Source may be on a different server (operated by you or a third party) that +supports equivalent copying facilities, provided you maintain clear directions +next to the object code saying where to find the Corresponding Source. Regardless +of what server hosts the Corresponding Source, you remain obligated to ensure +that it is available for as long as needed to satisfy these requirements. + +e) Convey the object code using peer-to-peer transmission, provided you inform +other peers where the object code and Corresponding Source of the work are +being offered to the general public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded from +the Corresponding Source as a System Library, need not be included in conveying +the object code work. + +A “User Product” is either (1) a “consumer product”, which means any tangible +personal property which is normally used for personal, family, or household +purposes, or (2) anything designed or sold for incorporation into a dwelling. +In determining whether a product is a consumer product, doubtful cases shall +be resolved in favor of coverage. For a particular product received by a particular +user, “normally used” refers to a typical or common use of that class of product, +regardless of the status of the particular user or of the way in which the +particular user actually uses, or expects or is expected to use, the product. +A product is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent the +only significant mode of use of the product. + +“Installation Information” for a User Product means any methods, procedures, +authorization keys, or other information required to install and execute modified +versions of a covered work in that User Product from a modified version of +its Corresponding Source. The information must suffice to ensure that the +continued functioning of the modified object code is in no case prevented +or interfered with solely because modification has been made. + +If you convey an object code work under this section in, or with, or specifically +for use in, a User Product, and the conveying occurs as part of a transaction +in which the right of possession and use of the User Product is transferred +to the recipient in perpetuity or for a fixed term (regardless of how the +transaction is characterized), the Corresponding Source conveyed under this +section must be accompanied by the Installation Information. But this requirement +does not apply if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has been installed +in ROM). + +The requirement to provide Installation Information does not include a requirement +to continue to provide support service, warranty, or updates for a work that +has been modified or installed by the recipient, or for the User Product in +which it has been modified or installed. Access to a network may be denied +when the modification itself materially and adversely affects the operation +of the network or violates the rules and protocols for communication across +the network. + +Corresponding Source conveyed, and Installation Information provided, in accord +with this section must be in a format that is publicly documented (and with +an implementation available to the public in source code form), and must require +no special password or key for unpacking, reading or copying. + +7. Additional Terms. +“Additional permissions” are terms that supplement the terms of this License +by making exceptions from one or more of its conditions. Additional permissions +that are applicable to the entire Program shall be treated as though they +were included in this License, to the extent that they are valid under applicable +law. If additional permissions apply only to part of the Program, that part +may be used separately under those permissions, but the entire Program remains +governed by this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option remove any +additional permissions from that copy, or from any part of it. (Additional +permissions may be written to require their own removal in certain cases when +you modify the work.) You may place additional permissions on material, added +by you to a covered work, for which you have or can give appropriate copyright +permission. + +Notwithstanding any other provision of this License, for material you add +to a covered work, you may (if authorized by the copyright holders of that +material) supplement the terms of this License with terms: + +a) Disclaiming warranty or limiting liability differently from the terms of +sections 15 and 16 of this License; or + +b) Requiring preservation of specified reasonable legal notices or author +attributions in that material or in the Appropriate Legal Notices displayed +by works containing it; or + +c) Prohibiting misrepresentation of the origin of that material, or requiring +that modified versions of such material be marked in reasonable ways as different +from the original version; or + +d) Limiting the use for publicity purposes of names of licensors or authors +of the material; or + +e) Declining to grant rights under trademark law for use of some trade names, +trademarks, or service marks; or + +f) Requiring indemnification of licensors and authors of that material by +anyone who conveys the material (or modified versions of it) with contractual +assumptions of liability to the recipient, for any liability that these contractual +assumptions directly impose on those licensors and authors. + +All other non-permissive additional terms are considered “further restrictions” +within the meaning of section 10. If the Program as you received it, or any +part of it, contains a notice stating that it is governed by this License +along with a term that is a further restriction, you may remove that term. +If a license document contains a further restriction but permits relicensing +or conveying under this License, you may add to a covered work material governed +by the terms of that license document, provided that the further restriction +does not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you must place, +in the relevant source files, a statement of the additional terms that apply +to those files, or a notice indicating where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the form +of a separately written license, or stated as exceptions; the above requirements +apply either way. + +8. Termination. +You may not propagate or modify a covered work except as expressly provided +under this License. Any attempt otherwise to propagate or modify it is void, +and will automatically terminate your rights under this License (including +any patent licenses granted under the third paragraph of section 11). + +However, if you cease all violation of this License, then your license from +a particular copyright holder is reinstated (a) provisionally, unless and +until the copyright holder explicitly and finally terminates your license, +and (b) permanently, if the copyright holder fails to notify you of the violation +by some reasonable means prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is reinstated permanently +if the copyright holder notifies you of the violation by some reasonable means, +this is the first time you have received notice of violation of this License +(for any work) from that copyright holder, and you cure the violation prior +to 30 days after your receipt of the notice. + +Termination of your rights under this section does not terminate the licenses +of parties who have received copies or rights from you under this License. +If your rights have been terminated and not permanently reinstated, you do +not qualify to receive new licenses for the same material under section 10. + +9. Acceptance Not Required for Having Copies. +You are not required to accept this License in order to receive or run a copy +of the Program. Ancillary propagation of a covered work occurring solely as +a consequence of using peer-to-peer transmission to receive a copy likewise +does not require acceptance. However, nothing other than this License grants +you permission to propagate or modify any covered work. These actions infringe +copyright if you do not accept this License. Therefore, by modifying or propagating +a covered work, you indicate your acceptance of this License to do so. + +10. Automatic Licensing of Downstream Recipients. +Each time you convey a covered work, the recipient automatically receives +a license from the original licensors, to run, modify and propagate that work, +subject to this License. You are not responsible for enforcing compliance +by third parties with this License. + +An “entity transaction” is a transaction transferring control of an organization, +or substantially all assets of one, or subdividing an organization, or merging +organizations. If propagation of a covered work results from an entity transaction, +each party to that transaction who receives a copy of the work also receives +whatever licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the Corresponding +Source of the work from the predecessor in interest, if the predecessor has +it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the rights +granted or affirmed under this License. For example, you may not impose a +license fee, royalty, or other charge for exercise of rights granted under +this License, and you may not initiate litigation (including a cross-claim +or counterclaim in a lawsuit) alleging that any patent claim is infringed +by making, using, selling, offering for sale, or importing the Program or +any portion of it. + +11. Patents. +A “contributor” is a copyright holder who authorizes use under this License +of the Program or a work on which the Program is based. The work thus licensed +is called the contributor's “contributor version”. + +A contributor's “essential patent claims” are all patent claims owned or controlled +by the contributor, whether already acquired or hereafter acquired, that would +be infringed by some manner, permitted by this License, of making, using, +or selling its contributor version, but do not include claims that would be +infringed only as a consequence of further modification of the contributor +version. For purposes of this definition, “control” includes the right to +grant patent sublicenses in a manner consistent with the requirements of this +License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free patent +license under the contributor's essential patent claims, to make, use, sell, +offer for sale, import and otherwise run, modify and propagate the contents +of its contributor version. + +In the following three paragraphs, a “patent license” is any express agreement +or commitment, however denominated, not to enforce a patent (such as an express +permission to practice a patent or covenant not to sue for patent infringement). +To “grant” such a patent license to a party means to make such an agreement +or commitment not to enforce a patent against the party. + +If you convey a covered work, knowingly relying on a patent license, and the +Corresponding Source of the work is not available for anyone to copy, free +of charge and under the terms of this License, through a publicly available +network server or other readily accessible means, then you must either (1) +cause the Corresponding Source to be so available, or (2) arrange to deprive +yourself of the benefit of the patent license for this particular work, or +(3) arrange, in a manner consistent with the requirements of this License, +to extend the patent license to downstream recipients. “Knowingly relying” +means you have actual knowledge that, but for the patent license, your conveying +the covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that country +that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or arrangement, +you convey, or propagate by procuring conveyance of, a covered work, and grant +a patent license to some of the parties receiving the covered work authorizing +them to use, propagate, modify or convey a specific copy of the covered work, +then the patent license you grant is automatically extended to all recipients +of the covered work and works based on it. + +A patent license is “discriminatory” if it does not include within the scope +of its coverage, prohibits the exercise of, or is conditioned on the non-exercise +of one or more of the rights that are specifically granted under this License. +You may not convey a covered work if you are a party to an arrangement with +a third party that is in the business of distributing software, under which +you make payment to the third party based on the extent of your activity of +conveying the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory patent +license (a) in connection with copies of the covered work conveyed by you +(or copies made from those copies), or (b) primarily for and in connection +with specific products or compilations that contain the covered work, unless +you entered into that arrangement, or that patent license was granted, prior +to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting any implied +license or other defenses to infringement that may otherwise be available +to you under applicable patent law. + +12. No Surrender of Others' Freedom. +If conditions are imposed on you (whether by court order, agreement or otherwise) +that contradict the conditions of this License, they do not excuse you from +the conditions of this License. If you cannot convey a covered work so as +to satisfy simultaneously your obligations under this License and any other +pertinent obligations, then as a consequence you may not convey it at all. +For example, if you agree to terms that obligate you to collect a royalty +for further conveying from those to whom you convey the Program, the only +way you could satisfy both those terms and this License would be to refrain +entirely from conveying the Program. + +13. Use with the GNU Affero General Public License. +Notwithstanding any other provision of this License, you have permission to +link or combine any covered work with a work licensed under version 3 of the +GNU Affero General Public License into a single combined work, and to convey +the resulting work. The terms of this License will continue to apply to the +part which is the covered work, but the special requirements of the GNU Affero +General Public License, section 13, concerning interaction through a network +will apply to the combination as such. + +14. Revised Versions of this License. +The Free Software Foundation may publish revised and/or new versions of the +GNU General Public License from time to time. Such new versions will be similar +in spirit to the present version, but may differ in detail to address new +problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies +that a certain numbered version of the GNU General Public License “or any +later version” applies to it, you have the option of following the terms and +conditions either of that numbered version or of any later version published +by the Free Software Foundation. If the Program does not specify a version +number of the GNU General Public License, you may choose any version ever +published by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future versions of +the GNU General Public License can be used, that proxy's public statement +of acceptance of a version permanently authorizes you to choose that version +for the Program. + +Later license versions may give you additional or different permissions. However, +no additional obligations are imposed on any author or copyright holder as +a result of your choosing to follow a later version. + +15. Disclaimer of Warranty. +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE +LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER +EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM +PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR +CORRECTION. + +16. Limitation of Liability. +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL +ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM +AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, +INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO +USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED +INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE +PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER +PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +17. Interpretation of Sections 15 and 16. +If the disclaimer of warranty and limitation of liability provided above cannot +be given local legal effect according to their terms, reviewing courts shall +apply local law that most closely approximates an absolute waiver of all civil +liability in connection with the Program, unless a warranty or assumption +of liability accompanies a copy of the Program in return for a fee. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible +use to the public, the best way to achieve this is to make it free software +which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach +them to the start of each source file to most effectively state the exclusion +of warranty; and each file should have at least the “copyright” line and a +pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + +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/>. + +Also add information on how to contact you by electronic and paper mail. + +If the program does terminal interaction, make it output a short notice like +this when it starts in an interactive mode: + + <program> Copyright (C) <year> <name of author> + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. +This is free software, and you are welcome to redistribute it under certain +conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands might +be different; for a GUI interface, you would use an “about box”. + +You should also get your employer (if you work as a programmer) or school, +if any, to sign a “copyright disclaimer” for the program, if necessary. For +more information on this, and how to apply and follow the GNU GPL, see <http://www.gnu.org/licenses/>. + +The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General Public +License instead of this License. But first, please read <http://www.gnu.org/philosophy/why-not-lgpl.html>. diff --git a/PreLoad.cmake b/PreLoad.cmake new file mode 100644 index 0000000..37029bd --- /dev/null +++ b/PreLoad.cmake @@ -0,0 +1 @@ +set (CMAKE_GENERATOR "Unix Makefiles" CACHE INTERNAL "" FORCE) diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..a8ab948 --- /dev/null +++ b/Readme.md @@ -0,0 +1,44 @@ +# Harbour-PassFish + +PassFish is a reimplementation of [PasswordMaker Pro](https://passwordmaker.org/) for Sailfish OS, that aims to be mostly compatible to +the [JavaScript edition](https://sourceforge.net/projects/passwordmaker/files/Javascript%20Edition/). + +All credit for the development of the PasswordMaker Pro algorithm (and therefore for the high level flow of this library too) goes to the original authors of PasswordMaker Pro, [Miquel Burns](https://github.com/miquelfire) and [Eric H. Jung](https://github.com/ericjung). (I really hope I linked the correct profiles.) + +The project consists of two parts, which are developed in tandem, but published under different licenses. +This part, the main application ("PassFish"), is licensed under the [GPL-3.0-or-later](https://spdx.org/licenses/GPL-3.0-or-later.html) (see LICENSE file). The library containing the PasswordMaker logic +("[passwordmaker-rs](https://github.com/soulsource/passwordmaker-rs)") is licensed under LGPLv3 or later. + +## Architecture +PassFish itself is mainly developed in Rust, with the GUI being defined in QML. The QML part also takes on the role of a Controller, as it is driving +the Rust logic. The glue between those two parts is mostly auto-generated using [rust-qt-binding-generator](https://invent.kde.org/sdk/rust-qt-binding-generator), +but a few lines of glue code are also written by hand. + +The Rust code can be roughly separated in three modules: The main part of the program, which is simply called passwordmaker, and which is responsible +for the interaction with the main page of the QML UI, the profiles, which are stored and read from disc, and the helperthread, which manages the actual +generation of the passwords. +Both, helperthread and passwordmaker modules interact with passwordmaker-rs. + +## Building +To be able to build this project you will (obviously) need the Sailfish SDK. +Last time I checked the Virtualbox based build engine still had issues with cargo, so (unless those have been resolved in the meantime) you might want to install the Docker based build engine. +See https://forum.sailfishos.org/t/rust-howto-request/3187/10 for further information. + +To install the Rust compiler in your Sailfish SDK build target, you can use the Sailfish SDK Qt Creator. The relevant dialogue is under "Settings" -> "Sailfish OS" -> "Manage Build Targets". For each build target, please install "cargo" and the rust standard library ("rust-std-static-<target>"). + +To update the Rust bindings (a build step in the project's CMakelists.txt) for Qt you'll also need to install [rust-qt-binding-generator](https://invent.kde.org/sdk/rust-qt-binding-generator) in the build engine VM. This project uses a modified version, available in the [mockall_support](https://invent.kde.org/soulsource/rust-qt-binding-generator/-/tree/mockall_support) branch. This branch is used to ease the creation of automated tests, and it includes some rather drastic changes to work around an undefined behaviour bug. + +The cmake file will find the rust_qt_binding_generator binary if it's either in the PATH or in /home/mersdk/bin. +Since the build engine is using a rather old C library, cross compiling rust-qt-binding-generator on the host might not work. When in doubt, you can just build it with the Sailfish build engine by running `sfdk engine exec sb2 -t SailfishOS-X.X.X.X-i486 cargo build`. You can also [remote into the build engine](https://docs.sailfishos.org/Tools/Sailfish_SDK/FAQ/#build-engine), and just build it there (using a temporary `zypper install cargo` install, but don't forget to uninstall it again afterwards). + +Last, but not least, I decided not to add any hacky auto-detect steps for the rust target triplet to CMakeLists.txt. Instead you need to give the value to cmake on the cmake command line. To do that with Qt Creator, open the Projects view and for each target and build configuration add the `-DRUST_TARGET_TRIPLET=some-target-triplet` setting to the "Initial CMake parameters" list. I've used the same triplets that are being used by the official [rust.spec](https://github.com/sailfishos/rust/blob/5a164a7d8f91fc147458222f92db35b7567bac1c/rust.spec#L35-L37) file and those seem to work fine. Then re-configure the target with the now changed initial parameters. + +With that all set up an running, you should be able to build the project using the Qt Creator that comes with the Sailfish SDK. + +## Notes for GUI development/porting +The model part of the application is (nearly) fully implemented in Rust, so adding a UI for another platform is as straightforward as adding new views (in either Qt Widgets and/or QML). + +There is one **important** thing to take care of when adding another UI implementation: The rust-qt-binding-generator does (at the time of writing) not offer any way to add custom signals/slots to the auto-generated QObjects, and also access to the QObject pointer is private on the Rust side. In other words: There is no way to set up connections from Rust. +Where I'm getting at is that if you create a new `PasswordMaker` object in either C++ or QML, you need to connect a signal: +The signal `i_say_sexy_things_to_myself_while_im_dancingChanged` needs to be connected to `set_i_say_sexy_things_to_myself_while_im_dancing`. See PassFish.cpp for +details. diff --git a/bindings.json b/bindings.json new file mode 100644 index 0000000..646a7f9 --- /dev/null +++ b/bindings.json @@ -0,0 +1,98 @@ +{ + "generate_automocks": true, + "cppFile": "src/Bindings.cpp", + "rust": { + "dir": "rust", + "interfaceModule": "interface", + "implementationModule": "implementation" + }, + "objects": { + "PasswordMaker" : { + "type" : "Object", + "properties" : { + "i_say_sexy_things_to_myself_while_im_dancing" : { + "type" : "bool", + "write": true + }, + "profiles" : { + "type" : "Profiles" + }, + "settings" : { + "type" : "Settings" + }, + "url" : { + "type" : "QString", + "write" : true + }, + "used_text" : { + "type" : "QString", + "write" : true + }, + "master_password" : { + "type" : "QString", + "write" : true + }, + "generated_password" : { + "type" : "QString" + }, + "generator_state" : { + "type" : "quint8" + } + }, + "functions" : { + "store_settings" : { + "return" : "bool", + "arguments" : [] + }, + "profile_changed" : { + "return": "void", + "arguments" : [] + } + } + }, + "Settings" : { + "type" : "Object", + "properties" : { + "clear_generated_password_seconds" : { "type" : "quint32", "optional": true, "write" : true }, + "clear_master_password_seconds" : { "type" : "quint32", "optional": true, "write" : true }, + "hide_generated_password" : { "type" : "bool", "write" :true } + } + }, + "Profiles" : { + "type" : "List", + "properties" : { + "current_profile" : { + "type" : "quint32", + "write" : true + }, + "current_profile_name" : { + "type" : "QString" + } + }, + "itemProperties" : { + "name" : { "type" : "QString", "write" : true, "roles" : [[ "display", "edit"]] }, + "use_leet" : { "type" : "quint8", "write" : true, "roles" : [[],[],[],[],[],[],[],[],[], [ "display", "edit"]] }, + "leet_level" : { "type" : "quint8", "write" : true, "roles" : [[],[],[],[],[],[],[],[],[],[], [ "display", "edit"]] }, + "hash_algorithm" : { "type" : "quint8", "write" : true, "roles" : [[],[],[],[],[],[],[],[], [ "display", "edit"]] }, + "use_protocol" : { "type" : "bool", "write" : true, "roles" : [[], [ "display", "edit"]] }, + "use_subdomains" : { "type" : "bool", "write" : true, "roles" : [[],[], [ "display", "edit"]] }, + "use_domain" : { "type" : "bool", "write" : true, "roles" : [[],[],[], [ "display", "edit"]] }, + "use_port_path" : { "type" : "bool", "write" : true, "roles" : [[],[],[],[], [ "display", "edit"]] }, + "use_user_info" : { "type" : "bool", "write" : true, "roles" : [[],[],[],[],[], [ "display", "edit"]] }, + "use_undefined_as_protocol_fallback" : { "type" : "bool", "write" : true, "roles" : [[],[],[],[],[],[], [ "display", "edit"]] }, + "password_length" : { "type" : "quint32", "write" : true, "roles" : [[],[],[],[],[],[],[], [ "display", "edit"]] }, + "username" : { "type" : "QString", "write" : true, "roles" : [[],[],[],[],[],[],[],[],[],[],[],[], [ "display", "edit"]] }, + "modifier" : { "type" : "QString", "write" : true, "roles" : [[],[],[],[],[],[],[],[],[],[],[],[],[], [ "display", "edit"]] }, + "characters" : { "type" : "QString", "write" : true, "roles" : [[],[],[],[],[],[],[],[],[],[],[], [ "display", "edit"]] }, + "prefix" : { "type" : "QString", "write" : true, "roles" : [[],[],[],[],[],[],[],[],[],[],[],[],[],[], [ "display", "edit"]] }, + "suffix" : { "type" : "QString", "write" : true, "roles" : [[],[],[],[],[],[],[],[],[],[],[],[],[],[],[], [ "display", "edit"]] } + }, + "functions" : { + "store" : { + "return" : "bool", + "arguments" : [] + } + } + } + } +} diff --git a/cmake/FindCargo.cmake b/cmake/FindCargo.cmake new file mode 100644 index 0000000..6627d86 --- /dev/null +++ b/cmake/FindCargo.cmake @@ -0,0 +1,10 @@ +include(FindPackageHandleStandardArgs) +find_program(Cargo_EXECUTABLE cargo) +execute_process(COMMAND "${Cargo_EXECUTABLE}" --version + OUTPUT_VARIABLE Cargo_VERSION_OUTPUT) +STRING(REGEX MATCH "[0-9]+\\.[0-9]+\\.[0-9]+" + Cargo_VERSION "${Cargo_VERSION_OUTPUT}") +find_package_handle_standard_args(Cargo + REQUIRED_VARS Cargo_EXECUTABLE + VERSION_VAR Cargo_VERSION) +mark_as_advanced(Cargo_EXECUTABLE) diff --git a/cmake/FindRust.cmake b/cmake/FindRust.cmake new file mode 100644 index 0000000..f87ca02 --- /dev/null +++ b/cmake/FindRust.cmake @@ -0,0 +1,13 @@ +include(FindPackageHandleStandardArgs) +find_program(Rust_EXECUTABLE rustc) +execute_process(COMMAND "${Rust_EXECUTABLE}" --version + OUTPUT_VARIABLE Rust_VERSION_OUTPUT) +STRING(REGEX MATCH "[0-9]+\\.[0-9]+\\.[0-9]+" + Rust_VERSION "${Rust_VERSION_OUTPUT}") +message(STATUS "Rust Exe=${Rust_EXECUTABLE}") +message(STATUS "Rust Version=${Rust_VERSION}") +message(STATUS "Full Rust Version= ${Rust_VERSION_OUTPUT}") +find_package_handle_standard_args(Rust + REQUIRED_VARS Rust_EXECUTABLE + VERSION_VAR Rust_VERSION) +mark_as_advanced(Rust_EXECUTABLE) diff --git a/cmake/FindRustQtBindingGenerator.cmake b/cmake/FindRustQtBindingGenerator.cmake new file mode 100644 index 0000000..dc345c0 --- /dev/null +++ b/cmake/FindRustQtBindingGenerator.cmake @@ -0,0 +1,5 @@ +include(FindPackageHandleStandardArgs) +find_program(RustQtBindingGenerator_EXECUTABLE rust_qt_binding_generator HINTS ~/bin) +find_package_handle_standard_args(RustQtBindingGenerator + REQUIRED_VARS RustQtBindingGenerator_EXECUTABLE) +mark_as_advanced(RustQtBindingGenerator_EXECUTABLE) diff --git a/harbour-passfish.desktop b/harbour-passfish.desktop new file mode 100644 index 0000000..4a5b108 --- /dev/null +++ b/harbour-passfish.desktop @@ -0,0 +1,21 @@ +[Desktop Entry] +Type=Application +Icon=harbour-passfish +Exec=harbour-passfish +Name=PassFish + +#commented out because it causes troubles on SFOS 3.4 +X-Nemo-Application-Type=silica-qt5 +#X-Nemo-Application-Type=generic + +# translation example: +# your app name in German locale (de) +# +# Remember to comment out the following line, if you do not want to use +# a different app name in German locale (de). +#Name[de]=PassFish + +[X-Sailjail] +Permissions= +OrganizationName=info.grois +ApplicationName=harbour-passfish diff --git a/icons/108x108/harbour-passfish.png b/icons/108x108/harbour-passfish.png Binary files differnew file mode 100644 index 0000000..ab10628 --- /dev/null +++ b/icons/108x108/harbour-passfish.png diff --git a/icons/128x128/harbour-passfish.png b/icons/128x128/harbour-passfish.png Binary files differnew file mode 100644 index 0000000..54375c5 --- /dev/null +++ b/icons/128x128/harbour-passfish.png diff --git a/icons/172x172/harbour-passfish.png b/icons/172x172/harbour-passfish.png Binary files differnew file mode 100644 index 0000000..36eee58 --- /dev/null +++ b/icons/172x172/harbour-passfish.png diff --git a/icons/86x86/harbour-passfish.png b/icons/86x86/harbour-passfish.png Binary files differnew file mode 100644 index 0000000..ad316d6 --- /dev/null +++ b/icons/86x86/harbour-passfish.png diff --git a/qml/PassFish.qml b/qml/PassFish.qml new file mode 100644 index 0000000..70e095d --- /dev/null +++ b/qml/PassFish.qml @@ -0,0 +1,10 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import "pages" + +ApplicationWindow { + initialPage: Component { MainPage { } } + cover: Qt.resolvedUrl("cover/CoverPage.qml") + allowedOrientations: defaultAllowedOrientations + _defaultPageOrientations: defaultAllowedOrientations +} diff --git a/qml/components/NoticeOptional.qml b/qml/components/NoticeOptional.qml new file mode 100644 index 0000000..83e0198 --- /dev/null +++ b/qml/components/NoticeOptional.qml @@ -0,0 +1,68 @@ +import QtQuick 2.6 +import Sailfish.Silica 1.0 + +/// Helper to allow using Notice without breaking compilation on SailfishOS 3.4. +/// This helper conditionally loads either a Notice or a Notification +/// (or nothing if useNotificationFallback is disabled) based on the availability +/// of the Notice type. +Loader { + property string text + property string duration : "Notice.Short" + property bool useNotificationFallback : true + property bool transientNotificationFallback : duration === "Notice.Short" + + readonly property bool noticesAvailable : typeof(Notices) !== "undefined" + + function show() { + asynchronous = false; + if(item){ + if(item.show) { + item.show(); + return; + } + else if(item.publish){ + item.publish(); + return; + } + } + console.log("Notice could not be shown: " + text); + } + + asynchronous: true + source: noticesAvailable + ? "../helpers/NoticeLoadable.qml" + : (useNotificationFallback ? "../helpers/NotificationLoadable.qml" : "") + + Binding { + target: item + property: "text" + value: text + when: noticesAvailable && status == Loader.Ready + } + Binding { + target: item + property: "duration" + value: duration + when: noticesAvailable && status == Loader.Ready + } + //---------------------------------------------------------- + //fallback bindings + Binding { + target: item + property: "summary" + value: text + when: !noticesAvailable && useNotificationFallback && status == Loader.Ready + } + Binding { + target: item + property: "previewSummary" + value: text + when: !noticesAvailable && useNotificationFallback && status == Loader.Ready + } + Binding { + target: item + property: "isTransient" + value: transientNotificationFallback + when: !noticesAvailable && useNotificationFallback && status == Loader.Ready + } +} diff --git a/qml/cover/CoverPage.qml b/qml/cover/CoverPage.qml new file mode 100644 index 0000000..5b5af6c --- /dev/null +++ b/qml/cover/CoverPage.qml @@ -0,0 +1,73 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +CoverBackground { + Column{ + x: Theme.paddingLarge + y: Theme.paddingLarge + width: parent.width - 2 * x + spacing: Theme.paddingLarge + Label { + id: label + text: qsTr("PassFish") + color: Theme.highlightColor + fontSizeMode: Text.Fit + font.pixelSize: Theme.fontSizeLarge + bottomPadding: Theme.paddingLarge + width: parent.width + elide: Text.ElideNone + } + Column{ + width: parent.width + spacing: Theme.paddingSmall + Label { + id: urlLabel + color: Theme.highlightColor + text: qsTr("Used Text:") + font.pixelSize: Theme.fontSizeSmall + truncationMode: TruncationMode.Fade + width: parent.width + } + Label { + id: generated_for_url + text: passwordmaker.used_text + font.pixelSize: Theme.fontSizeSmall + truncationMode: TruncationMode.Fade + width: parent.width + } + } + Column{ + width: parent.width + spacing: Theme.paddingSmall + Label { + id: readyLabel + color: Theme.highlightColor + text: passwordmaker.generated_password.length > 0 + && passwordmaker.generator_state === 0 + ? qsTr("Pass Ready") + : qsTr("Input Needed") + font.pixelSize: Theme.fontSizeSmall + truncationMode: TruncationMode.Fade + width: parent.width + } + } + } + + CoverActionList { + id: coverAction + enabled: passwordmaker.generated_password.length > 0 + && passwordmaker.generator_state === 0 + CoverAction { + iconSource: "image://theme/icon-s-clipboard" + onTriggered: Clipboard.text = passwordmaker.generated_password; + } + + CoverAction { + iconSource: "image://theme/icon-cover-cancel" + onTriggered: { + passwordmaker.master_password = ""; + passwordmaker.url = ""; + } + } + } +} diff --git a/qml/helpers/NoticeLoadable.qml b/qml/helpers/NoticeLoadable.qml new file mode 100644 index 0000000..2b1226b --- /dev/null +++ b/qml/helpers/NoticeLoadable.qml @@ -0,0 +1,8 @@ +import QtQuick 2.6 +import Sailfish.Silica 1.0 + +//Just a wrapper around Notice, because I need to use it in a ternary in loader and don't want to pass in a string representation. +//See components/NoticeOptional.qml for details. If you know a simple and not too ugly solution to make it work without this file: +//I'm all ears. + +Notice {} diff --git a/qml/helpers/NotificationLoadable.qml b/qml/helpers/NotificationLoadable.qml new file mode 100644 index 0000000..1e44148 --- /dev/null +++ b/qml/helpers/NotificationLoadable.qml @@ -0,0 +1,10 @@ +import QtQuick 2.6 +import Sailfish.Silica 1.0 +import Nemo.Notifications 1.0 + +//Just a wrapper around Notification, because I need to use it in a ternary in loader and don't want to pass in a string representation. +//See components/NoticeOptional.qml for details. If you know a simple and not too ugly solution to make it work without this file: +//I'm all ears. + +Notification { +} diff --git a/qml/pages/AboutPage.qml b/qml/pages/AboutPage.qml new file mode 100644 index 0000000..7326fe4 --- /dev/null +++ b/qml/pages/AboutPage.qml @@ -0,0 +1,91 @@ +import QtQuick 2.6 +import Sailfish.Silica 1.0 + +Page { + id: aboutPage + SilicaFlickable { + anchors.fill: parent + contentHeight: column.height + contentWidth: parent.width + Column { + id: column + width: aboutPage.width + spacing: Theme.paddingLarge + bottomPadding: Theme.paddingLarge + + PageHeader { + title: qsTr("About PassFish") + } + Label { + width: parent.width - 2*Theme.horizontalPageMargin + x: Theme.horizontalPageMargin + wrapMode: Text.WordWrap + textFormat: Text.RichText + onLinkActivated : Qt.openUrlExternally(link) + color: Theme.highlightColor + text: qsTr(" +<p> + PassFish is a native re-implementation of <a href=\"https://passwordmaker.org/\">PasswordMaker</a> for Sailfish OS. + It aims to be mostly compatible to the <a href=\"https://sourceforge.net/projects/passwordmaker/files/Javascript%20Edition/\">Javascript Edition</a>. +</p> +<p> + All credit for the development of the PasswordMaker Pro algorithm goes to the original authors of PasswordMaker Pro, Miquel Burns and Eric H. Jung. +</p> +<p> + 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 + <a href=\"https://github.com/soulsource/passfish/issues\">github issue tracker</a> of the project. +</p> +<p> + 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). +</p> +<p> + This program consists of two parts: The application itself (\"<a href=\"https://github.com/soulsource/passfish\">PassFish</a>\"), and a Rust crate that contains the + implementation of the business logic (\"<a href=\"https://github.com/soulsource/passwordmaker-rs\"><nobr>passwordmaker‑rs</nobr></a>\"). + This is important, as the two parts use different licenses. PassFish is published under the GPLv3 license, while <nobr>passwordmaker‑rs</nobr> is published under LGPLv3. Please check the linked github pages for more details. +</p> +<p> + 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 <i>this</i> project, most libraries are available under multiple licenses. Please see the libraries' websites for details.<br> + These libraries are linked dynamically: + <ul> + <li><a href=\"https://www.qt.io/\">Qt Quick</a>: Used under <a href=\"https://www.gnu.org/licenses/lgpl-3.0.en.html\">LGPLv3</a></li> + <li><a href=\"https://sailfishos.org/develop/docs/silica/\">Sailfish Silica</a>: Proprietary, <a href=\"https://www.gnu.org/licenses/gpl-3.0\">GPL</a> system library exception</li> + <li><a href=\"https://github.com/sailfishos/libsailfishapp\">LibSailfishApp</a>: Used under LGPL 2.1</li> + </ul> + These libraries and their dependencies are linked statically: + <ul> + <li><a href=\"https://crates.io/crates/libc\">libc Rust bindings</a>: Used under MIT license</li> + <li><a href=\"https://crates.io/crates/serde\">serde</a>: Used under MIT license</li> + <li><a href=\"https://crates.io/crates/toml\">toml-rs</a>: Used under MIT license</li> + <li><a href=\"https://crates.io/crates/dirs\">dirs</a>: Used under MIT license</li> + <li><a href=\"https://crates.io/crates/ripemd\">RustCrypto: RIPEMD</a>: Used under MIT license</li> + </ul> + The <nobr>passwordmaker‑rs</nobr> library has the following statically linked runtime dependencies: + <ul> + <li><a href=\"https://crates.io/crates/unicode-segmentation\">unicode-segmentation</a>: Used under MIT license</li> + </ul> + While it is not a runtime dependency, the code generator for the Rust Qt bindings is worth noting: + <ul> + <li><a href=\"https://invent.kde.org/sdk/rust-qt-binding-generator\">Rust Qt Binding Generator</a></li> + </ul> + PassFish uses a modified version, which can be found in the <a href=\"https://invent.kde.org/soulsource/rust-qt-binding-generator/-/tree/mockall_support\">mockall_support</a> branch. +</p> +<p> + 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 <a href=\"http://www.gnu.org/licenses/\">http://www.gnu.org/licenses/</a>. +</p> + ") + } + } + + 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 <author's email> version-release +# - Summary of changes + +* Sun Apr 13 2014 Jack Tar <jack.tar@example.com> 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<std::path::PathBuf> { + dirs::config_dir() + .map(|p| p.join("info.grois/harbour-passfish/")) +} + +#[derive(Debug)] +enum LoadError { + Xdg, + Loading(std::io::Error), + Parsing(toml::de::Error), +} +impl std::fmt::Display for LoadError { + fn fmt(&self, f : &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + match self { + LoadError::Xdg => { write!(f, "XDG config path not found") } + LoadError::Loading(e) => { e.fmt(f) } + LoadError::Parsing(e) => { e.fmt(f) } + } + } +} +impl std::error::Error for LoadError {} +impl From<std::io::Error> for LoadError { + fn from(e : std::io::Error) -> Self { + LoadError::Loading(e) + } +} +impl From<toml::de::Error> for LoadError { + fn from(e : toml::de::Error) -> Self { + LoadError::Parsing(e) + } +} + + +#[derive(Debug)] +enum StoreError { + Xdg, + Writing(std::io::Error), + Serialization(toml::ser::Error) +} +impl std::fmt::Display for StoreError { + fn fmt(&self, f : &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + match self { + StoreError::Xdg => { write!(f, "XDC config path not found") } + StoreError::Writing(e) => { e.fmt(f) } + StoreError::Serialization(e) => { e.fmt(f) } + } + } +} +impl std::error::Error for StoreError {} +impl From<std::io::Error> for StoreError { + fn from(e: std::io::Error) -> Self { + StoreError::Writing(e) + } +} +impl From<toml::ser::Error> for StoreError { + fn from(e: toml::ser::Error) -> Self { + StoreError::Serialization(e) + } +} diff --git a/rust/src/implementation/passwordmaker/emittingsender.rs b/rust/src/implementation/passwordmaker/emittingsender.rs new file mode 100644 index 0000000..1cf58fc --- /dev/null +++ b/rust/src/implementation/passwordmaker/emittingsender.rs @@ -0,0 +1,17 @@ +use std::sync::mpsc::{channel,Sender,Receiver,SendError}; +#[derive(Clone)] +pub(crate) struct EmittingSender<MT, E : Fn()> { + sender : Sender<MT>, + emit : E, +} + +impl <MT, E : Fn()> EmittingSender<MT, E>{ + pub(crate) fn emitting_channel(emit : E) -> (Self, Receiver<MT>) { + let (sender,r) = channel(); + (EmittingSender { sender, emit }, r) + } + pub(crate) fn send(&self, t: MT) -> Result<(), SendError<MT>> { + //first send, then notify. + self.sender.send(t).map(|_| (self.emit)()) + } +} diff --git a/rust/src/implementation/passwordmaker/helperthread/hashers/mod.rs b/rust/src/implementation/passwordmaker/helperthread/hashers/mod.rs new file mode 100644 index 0000000..85fbc39 --- /dev/null +++ b/rust/src/implementation/passwordmaker/helperthread/hashers/mod.rs @@ -0,0 +1,31 @@ +mod qt_hash; +use passwordmaker_rs::{Hasher, HasherList}; +use ripemd::Digest; + + +pub(super) use qt_hash::{QtMd4, QtMd5, QtSha1, QtSha256}; +pub(super) struct Ripemd160; + +impl Hasher for Ripemd160{ + type Output = [u8;20]; + + fn hash(input : &[u8]) -> Self::Output { + let hash = ripemd::Ripemd160::digest(input); + hash.into() + } +} + +pub(super) struct PassFishHashers; +impl HasherList for PassFishHashers { + type MD4 = QtMd4; + type MD5 = QtMd5; + type SHA1 = QtSha1; + type SHA256 = QtSha256; + type RIPEMD160 = Ripemd160; +} + +impl passwordmaker_rs::Md4 for QtMd4 {} +impl passwordmaker_rs::Md5 for QtMd5 {} +impl passwordmaker_rs::Sha1 for QtSha1 {} +impl passwordmaker_rs::Sha256 for QtSha256 {} +impl passwordmaker_rs::Ripemd160 for Ripemd160 {} diff --git a/rust/src/implementation/passwordmaker/helperthread/hashers/qt_hash.rs b/rust/src/implementation/passwordmaker/helperthread/hashers/qt_hash.rs new file mode 100644 index 0000000..64d4e80 --- /dev/null +++ b/rust/src/implementation/passwordmaker/helperthread/hashers/qt_hash.rs @@ -0,0 +1,131 @@ +use std::borrow::{BorrowMut, Borrow}; +use passwordmaker_rs::Hasher; +use libc::size_t; + +pub(crate) struct Md4; +pub(crate) struct Md5; +pub(crate) struct Sha1; +pub(crate) struct Sha256; + +pub(crate) struct QHasher<T>(T); + +pub(crate) type QtMd4 = QHasher<Md4>; +pub(crate) type QtMd5 = QHasher<Md5>; +pub(crate) type QtSha1 = QHasher<Sha1>; +pub(crate) type QtSha256 = QHasher<Sha256>; + +impl<T> Hasher for QHasher<T> where T:QtHasher { + type Output = T::Output; + + fn hash(input : &[u8]) -> Self::Output { + let mut result = T::Output::default(); + let required_bytes = result.borrow().len(); + let computed_bytes = unsafe{ + pwm_qhash( + T::QT_ALGO_NUMBER, + input.as_ptr(), + input.len() as size_t, + result.borrow_mut().as_mut_ptr(), + required_bytes) + }; + assert_eq!(computed_bytes, required_bytes); //no point to forward this to caller. It's a code bug, plain and simple. + result + } +} + +pub(crate) trait QtHasher{ + type Output : Default + BorrowMut<[u8]>; + const QT_ALGO_NUMBER : size_t; +} + +impl QtHasher for Md4 { + type Output = [u8;16]; + const QT_ALGO_NUMBER : size_t = 0; +} + +impl QtHasher for Md5 { + type Output = [u8;16]; + const QT_ALGO_NUMBER : size_t = 1; +} + +impl QtHasher for Sha1 { + type Output = [u8;20]; + const QT_ALGO_NUMBER : size_t = 2; +} + +impl QtHasher for Sha256 { + type Output = [u8;32]; + const QT_ALGO_NUMBER : size_t = 4; +} + +#[cfg(test)] +use rust_testhelper::pwm_qhash; +#[cfg(not(test))] +extern "C"{ + fn pwm_qhash(algorithm : size_t, input : *const u8, input_length : size_t, output : *mut u8, output_capacity : size_t) -> size_t; +} + +/// Those tests are testing the integration of Qt's QCryptographicHash function. They are functional tests, NOT unit tests. +#[cfg(test)] +mod qt_hash_tests{ + use super::*; + fn get_simple_string_as_bytes() -> &'static [u8] { + "I am a simple string and I like simple things. I like dancing in the rain, I like eating hamburgers, and I like you.".as_bytes() + } + fn get_complex_string_as_bytes() -> &'static [u8] { + "I am a complex string and do complex stuff. I like 🕺 in the 🌧️. I like eating 🍔, and I ❤️ you.".as_bytes() + } + #[test] + fn md4_simple_string_test(){ + let hash = QtMd4::hash(get_simple_string_as_bytes()); + let expected = vec![0x14, 0x1f, 0x1f, 0x1d, 0xef, 0x4a, 0x0d, 0x15, 0x1d, 0xb5, 0x5f, 0x7c, 0xb8, 0x96, 0xca, 0x99]; + assert_eq!(hash.borrow(), expected); + } + #[test] + fn md4_complex_string_test(){ + let hash = QtMd4::hash(get_complex_string_as_bytes()); + let expected = vec![0xc1, 0xfb, 0xe3, 0x4d, 0x72, 0x75, 0xb3, 0xa5, 0x3a, 0xc3, 0x45, 0xcf, 0x90, 0x34, 0x81, 0xf7]; + assert_eq!(hash.borrow(), expected); + } + + #[test] + fn md5_simple_string_test(){ + let hash = QtMd5::hash(get_simple_string_as_bytes()); + let expected = vec![0x4b, 0x2e, 0x18, 0x22, 0x45, 0x43, 0xf3, 0x96, 0xee, 0x79, 0x53, 0x18, 0x90, 0x1b, 0xb9, 0x7f]; + assert_eq!(hash.borrow(), expected); + } + #[test] + fn md5_complex_string_test(){ + let hash = QtMd5::hash(get_complex_string_as_bytes()); + let expected = vec![0x70, 0x31, 0x35, 0xd9, 0x38, 0x55, 0x1d, 0x2a, 0xae, 0xfa, 0xd9, 0x38, 0x07, 0x91, 0x11, 0xfe ]; + assert_eq!(hash.borrow(), expected); + } + + #[test] + fn sha1_simple_string_test(){ + let hash = QtSha1::hash(get_simple_string_as_bytes()); + let expected = vec![0xa1, 0x0a, 0x15, 0x18, 0x99, 0x29, 0x9d, 0xc7, 0xa6, 0x48, 0x36, 0x11, 0x44, 0xb3, 0x94, 0x09, 0x87, 0x3a, 0x39, 0xf3]; + assert_eq!(hash.borrow(), expected); + } + #[test] + fn sha1_complex_string_test(){ + let hash = QtSha1::hash(get_complex_string_as_bytes()); + let expected = vec![0x30, 0x79, 0xcd, 0xbc, 0x66, 0x09, 0xad, 0x24, 0x99, 0x44, 0xe5, 0x52, 0x25, 0xdf, 0xb4, 0x68, 0xfd, 0x5f, 0xb9, 0x8f ]; + assert_eq!(hash.borrow(), expected); + } + + #[test] + fn sha256_simple_string_test(){ + let hash = QtSha256::hash(get_simple_string_as_bytes()); + let expected = vec![0xd4, 0xaf, 0x13, 0x6a, 0x87, 0x62, 0x12, 0xf1, 0x93, 0x7d, 0xd1, 0x71, 0xab, 0xa1, 0xfa, 0x3e, 0x3b, 0x8e, 0xc5, 0x68, + 0xed, 0x42, 0x46, 0x9d, 0xf0, 0x9b, 0xd0, 0xd8, 0xd8, 0x39, 0x09, 0x93]; + assert_eq!(hash.borrow(), expected); + } + #[test] + fn sha256_complex_string_test(){ + let hash = QtSha256::hash(get_complex_string_as_bytes()); + let expected = vec![0x01, 0x3d, 0x93, 0x17, 0x45, 0x18, 0x29, 0x41, 0x6a, 0x09, 0xb5, 0x65, 0x1b, 0x81, 0x32, 0x88, 0xce, 0x83, 0xad, 0x92, + 0x04, 0x0f, 0x24, 0x13, 0x57, 0x8d, 0xd1, 0xa5, 0xe8, 0x3a, 0x73, 0xaa ]; + assert_eq!(hash.borrow(), expected); + } +}
\ No newline at end of file diff --git a/rust/src/implementation/passwordmaker/helperthread/message_parsing.rs b/rust/src/implementation/passwordmaker/helperthread/message_parsing.rs new file mode 100644 index 0000000..4cda572 --- /dev/null +++ b/rust/src/implementation/passwordmaker/helperthread/message_parsing.rs @@ -0,0 +1,102 @@ +use std::sync::mpsc::{Receiver, RecvError}; +use super::super::thread_messages::UiToHelper; + +pub(super) fn receive_and_get_newest_or_important_command(receiver : &Receiver<UiToHelper>) -> Result<NewestMostImportantCommand, RecvError> { + let first_message = receive_and_log_error(receiver)?; + Ok(get_newest_or_important_command(first_message, receiver.try_iter())) +} + +pub(super) struct NewestMostImportantCommand(pub UiToHelper); + +trait GetNewerOrMoreImportantCommand { + type Output; + fn get_newer_or_more_important_command(old: Self::Output, new: Self) -> Self::Output; + fn convert_first_command(self) -> Self::Output; +} + +impl GetNewerOrMoreImportantCommand for UiToHelper { + type Output = NewestMostImportantCommand; + + fn convert_first_command(self) -> Self::Output{ + NewestMostImportantCommand(self) + } + fn get_newer_or_more_important_command(old: Self::Output, new: Self) -> Self::Output { + match old.0 { + UiToHelper::Shutdown => old, + UiToHelper::GeneratePassword(_) => NewestMostImportantCommand(new), + } + } +} + + +fn get_newest_or_important_command<I, It >(first_message: I, other_messages: It) -> I::Output + where I: GetNewerOrMoreImportantCommand, + It: Iterator<Item=I> +{ + other_messages.fold(first_message.convert_first_command(), I::get_newer_or_more_important_command) +} + +fn receive_and_log_error(receiver : &Receiver<UiToHelper>) -> Result<UiToHelper, RecvError> { + match receiver.recv() { + Ok(x) => Ok(x), + e => { + eprintln!("Connection to UI Thread closed unexpectedly."); + e + } + } +} + +#[cfg(test)] +mod message_parsing_tests { + use super::*; + + #[derive(PartialEq, Debug)] + enum TestPrioritizedEnum { + Lowest(usize), + Highest(usize), + Medium(usize) + } + impl TestPrioritizedEnum{ + fn get_prio(&self) -> usize{ + match self { + TestPrioritizedEnum::Lowest(_) => 0, + TestPrioritizedEnum::Highest(_) => 2, + TestPrioritizedEnum::Medium(_) => 1, + } + } + } + + struct TestPrioritizedEnumPrioResult(TestPrioritizedEnum); + impl GetNewerOrMoreImportantCommand for TestPrioritizedEnum { + type Output = TestPrioritizedEnumPrioResult; + + fn get_newer_or_more_important_command(old: Self::Output, new: Self) -> Self::Output { + if old.0.get_prio() > new.get_prio() { old } else { TestPrioritizedEnumPrioResult(new) } + } + + fn convert_first_command(self) -> Self::Output { + TestPrioritizedEnumPrioResult(self) + } + } + + #[test] + fn test_get_newest_or_important_command_test(){ + let input1 = vec![TestPrioritizedEnum::Lowest(0), TestPrioritizedEnum::Lowest(1), TestPrioritizedEnum::Lowest(2)]; + let expected1 = TestPrioritizedEnum::Lowest(2); + let input2 = vec![TestPrioritizedEnum::Medium(0), TestPrioritizedEnum::Lowest(1), TestPrioritizedEnum::Lowest(2)]; + let expected2 = TestPrioritizedEnum::Medium(0); + let input3 = vec![TestPrioritizedEnum::Lowest(0), TestPrioritizedEnum::Medium(1), TestPrioritizedEnum::Lowest(2)]; + let expected3 = TestPrioritizedEnum::Medium(1); + let input4 = vec![TestPrioritizedEnum::Medium(0), TestPrioritizedEnum::Lowest(1), TestPrioritizedEnum::Medium(2)]; + let expected4 = TestPrioritizedEnum::Medium(2); + let input5 = vec![TestPrioritizedEnum::Lowest(0), TestPrioritizedEnum::Lowest(1), TestPrioritizedEnum::Highest(2), TestPrioritizedEnum::Medium(3)]; + let expected5 = TestPrioritizedEnum::Highest(2); + let data = vec![(input1, expected1), (input2, expected2), (input3, expected3), (input4, expected4), (input5, expected5)]; + for (input, expected) in data { + let mut it = input.into_iter(); + let first = it.next().unwrap(); + let result = get_newest_or_important_command(first,it); + assert_eq!(result.0, expected); + } + } +}
\ No newline at end of file diff --git a/rust/src/implementation/passwordmaker/helperthread/mod.rs b/rust/src/implementation/passwordmaker/helperthread/mod.rs new file mode 100644 index 0000000..7e3b587 --- /dev/null +++ b/rust/src/implementation/passwordmaker/helperthread/mod.rs @@ -0,0 +1,49 @@ +mod message_parsing; +mod hashers; +mod profile_to_domain; +use std::sync::mpsc::{Receiver, SendError}; +use passwordmaker_rs::PasswordMaker; +use profile_to_domain::{convert_hash_algorithm, convert_leet}; +use super::emittingsender::EmittingSender; +use super::thread_messages::{HelperToUi, UiToHelper}; +use message_parsing::receive_and_get_newest_or_important_command; +use hashers::PassFishHashers; + +pub(super) fn run<T: Fn()>(to_ui : &EmittingSender<HelperToUi, T>, from_ui : &Receiver<UiToHelper>) -> Result<(), SendError<HelperToUi>>{ + println!("Helper Thread starting up."); + + while let Ok(m) = receive_and_get_newest_or_important_command(from_ui) + { + //m is a product type, because we gather the latest command of each type. Easiest to deal with those would be using a group of ifs. + //for now the number of alternatives is rather small though, so let's match instead. + match m.0 { + UiToHelper::Shutdown => break, + UiToHelper::GeneratePassword(task) => { + + to_ui.send(HelperToUi::GenerationStarted)?; + let hash_algorithm = convert_hash_algorithm(&task.generation_settings.hash_algorithm); + let use_leet = convert_leet(&task.generation_settings.leet); + let characters = &task.generation_settings.characters; + let username = &task.generation_settings.username; + let modifier = &task.generation_settings.modifier; + let password_length = task.generation_settings.password_length as usize; + let suffix = &task.generation_settings.suffix; + let prefix = &task.generation_settings.prefix; + type Pwm<'a> = PasswordMaker<'a, PassFishHashers>; + let pwm = Pwm::new(hash_algorithm, use_leet, characters, username, modifier, password_length, prefix, suffix); + let input = task.input; + let master_password = task.master_password; + let pwm = pwm.map_err(Into::into); + let password = + pwm.and_then(|pwm| pwm.generate(input, master_password).map_err(Into::into)); + + match password { + Ok(password) => to_ui.send(HelperToUi::Generated{password})?, + Err(error) => to_ui.send(HelperToUi::GenerationFailed{ error })?, + } + } + } + } + println!("Helper Thread shutting down."); + Ok(()) +} diff --git a/rust/src/implementation/passwordmaker/helperthread/profile_to_domain.rs b/rust/src/implementation/passwordmaker/helperthread/profile_to_domain.rs new file mode 100644 index 0000000..152ba8f --- /dev/null +++ b/rust/src/implementation/passwordmaker/helperthread/profile_to_domain.rs @@ -0,0 +1,46 @@ +/// This whole module may look dumb, but that might change, once the public interface of passwordmaker_rs starts to deviate from the saved data. + +use passwordmaker_rs::{HashAlgorithm,UseLeetWhenGenerating,LeetLevel}; + +pub(super) fn convert_hash_algorithm(stored_algo : &crate::implementation::profiles::HashAlgorithm) -> HashAlgorithm { + use crate::implementation::profiles::HashAlgorithm as PHashAlgorithm; + match stored_algo { + PHashAlgorithm::Md4 => HashAlgorithm::Md4, + PHashAlgorithm::HmacMd4 => HashAlgorithm::HmacMd4, + PHashAlgorithm::Md5 => HashAlgorithm::Md5, + PHashAlgorithm::Md5Version06 => HashAlgorithm::Md5Version06, + PHashAlgorithm::HmacMd5 => HashAlgorithm::HmacMd5, + PHashAlgorithm::HmacMd5Version06 => HashAlgorithm::HmacMd5Version06, + PHashAlgorithm::Sha1 => HashAlgorithm::Sha1, + PHashAlgorithm::HmacSha1 => HashAlgorithm::HmacSha1, + PHashAlgorithm::Sha256 => HashAlgorithm::Sha256, + PHashAlgorithm::HmacSha256 => HashAlgorithm::HmacSha256, + PHashAlgorithm::Ripemd160 => HashAlgorithm::Ripemd160, + PHashAlgorithm::HmacRipemd160 => HashAlgorithm::HmacRipemd160, + } +} + +pub(super) fn convert_leet(stored_leet : &crate::implementation::profiles::UseLeetWhenGenerating) -> UseLeetWhenGenerating { + use crate::implementation::profiles::UseLeetWhenGenerating as PUseLeetWhenGenerating; + match stored_leet { + PUseLeetWhenGenerating::NotAtAll => UseLeetWhenGenerating::NotAtAll, + PUseLeetWhenGenerating::Before { level } => UseLeetWhenGenerating::Before { level: convert_leet_level(level) }, + PUseLeetWhenGenerating::After { level } => UseLeetWhenGenerating::After { level: convert_leet_level(level) }, + PUseLeetWhenGenerating::BeforeAndAfter { level } => UseLeetWhenGenerating::BeforeAndAfter { level: convert_leet_level(level) }, + } +} + +fn convert_leet_level(stored_level : &crate::implementation::profiles::LeetLevel) -> LeetLevel { + use crate::implementation::profiles::LeetLevel as PLeetLevel; + match stored_level { + PLeetLevel::One => LeetLevel::One, + PLeetLevel::Two => LeetLevel::Two, + PLeetLevel::Three => LeetLevel::Three, + PLeetLevel::Four => LeetLevel::Four, + PLeetLevel::Five => LeetLevel::Five, + PLeetLevel::Six => LeetLevel::Six, + PLeetLevel::Seven => LeetLevel::Seven, + PLeetLevel::Eight => LeetLevel::Eight, + PLeetLevel::Nine => LeetLevel::Nine, + } +}
\ No newline at end of file diff --git a/rust/src/implementation/passwordmaker/mod.rs b/rust/src/implementation/passwordmaker/mod.rs new file mode 100644 index 0000000..af0516c --- /dev/null +++ b/rust/src/implementation/passwordmaker/mod.rs @@ -0,0 +1,305 @@ +mod emittingsender; +mod thread_messages; +mod settings; +mod helperthread; + +pub use self::settings::Settings; +use passwordmaker_rs::{UrlParsing, ProtocolUsageMode, SettingsError, GenerationError}; +use crate::interface::ProfilesTrait; + +use std::cell::RefCell; +use std::convert::TryFrom; +use std::error::Error; +use std::fmt::Display; +use std::sync::mpsc::{Receiver, Sender, channel}; +use crate::implementation::pwm_macros::EnumVariantCount; + +use mockall_double::double; + +#[double] +use crate::interface::PasswordMakerEmitter; +use crate::interface::PasswordMakerTrait; +use super::Profiles; +use self::emittingsender::EmittingSender; +use self::thread_messages::{GeneratePasswordTask, HelperToUi, UiToHelper, GenerationIssue}; + +#[derive(EnumVariantCount, Clone, Copy)] +enum GeneratorState{ + MissingTextToUse, + MissingMasterPassword, + CharsetError, + GenerationCompleted, + Busy +} + +pub struct PasswordMaker { + emit : PasswordMakerEmitter, + profiles : Profiles, + settings : Settings, + url : RefCell<String>, + used_text : RefCell<String>, + master_password : RefCell<String>, + generated_password : RefCell<String>, + generator_state : RefCell<GeneratorState>, + from_helper : Receiver<HelperToUi>, + to_helper : Sender<UiToHelper>, + helper_thread : Option<std::thread::JoinHandle<()>> +} + +#[derive(Debug)] +struct GeneratorStateFromIntError; +impl Display for GeneratorStateFromIntError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f,"Invalid value for GeneratorStateFromIntError.") + } +} +impl Error for GeneratorStateFromIntError{} + +impl TryFrom<u8> for GeneratorState { + type Error = GeneratorStateFromIntError; + fn try_from(value: u8) -> Result<Self, Self::Error> { + match value { + 0 => Ok(GeneratorState::GenerationCompleted), + 1 => Ok(GeneratorState::Busy), + 2 => Ok(GeneratorState::MissingTextToUse), + 3 => Ok(GeneratorState::MissingMasterPassword), + 4 => Ok(GeneratorState::CharsetError), + 5..=255 => Err(GeneratorStateFromIntError) + } + } +} +impl From<GeneratorState> for u8 { + fn from(s : GeneratorState) -> Self { + match s { + GeneratorState::MissingTextToUse => 2, + GeneratorState::GenerationCompleted => 0, + GeneratorState::Busy => 1, + GeneratorState::MissingMasterPassword => 3, + GeneratorState::CharsetError => 4, + } + } +} + +impl PasswordMakerTrait for PasswordMaker { + fn new(emit : PasswordMakerEmitter, profiles : Profiles, settings : Settings) -> Self { + let stored_settings_data = settings.load(); + if let Ok(x) = &stored_settings_data { + #[allow(clippy::cast_possible_truncation)] + profiles.set_current_profile(x.current_profile_index as u32); + } + let settings = if let Ok(x) = stored_settings_data { x.settings } else { settings }; + + let emit_clone = emit.clone(); + let (to_ui,from_helper) = EmittingSender::emitting_channel( + move || {emit_clone.i_say_sexy_things_to_myself_while_im_dancing_changed();} + ); + let (to_helper, from_ui) = channel(); + let helper_thread = Some(std::thread::spawn(move || helperthread::run(&to_ui, &from_ui).expect("UI Thread hung up.") )); + PasswordMaker{emit, profiles, settings, from_helper, to_helper, helper_thread, + url: RefCell::new(String::new()), used_text: RefCell::new(String::new()), master_password: RefCell::new(String::new()), generated_password: RefCell::new(String::new()), generator_state: RefCell::new(GeneratorState::MissingTextToUse) } + } + fn emit(&self) -> &PasswordMakerEmitter { + &self.emit + } + fn profiles(&self) -> &Profiles { + &self.profiles + } + fn settings(&self) -> &Settings { + &self.settings + } + fn store_settings(&self) -> bool { + self.settings.store(self.profiles.current_profile() as usize).is_ok() + } + fn i_say_sexy_things_to_myself_while_im_dancing(&self) -> bool { + unreachable!() + } + fn set_i_say_sexy_things_to_myself_while_im_dancing(&self, _ : bool) { + if let Ok(m) = self.from_helper.try_recv() { + self.handle_message_from_helper(m); + } else { + println!("Spurious wakeup from helper thread. No message queued. Please investigate."); + } + } + + fn generated_password<F>(&self, setter: F) where F: FnOnce(&str) { + setter(&*self.generated_password.borrow()); + } + + fn master_password<F>(&self, setter: F) where F: FnOnce(&str) { + setter(&*self.master_password.borrow()); + } + + fn set_master_password(&self, value: String) { + let different = { + let mut b = self.master_password.borrow_mut(); + let different = *b != value; + if different {*b = value;} + different + }; + if different { + self.update_generated_password(); + self.emit().master_password_changed(); + } + } + + fn url<F>(&self, setter: F) where F: FnOnce(&str) { + setter(&*self.url.borrow()); + } + + fn set_url(&self, value: String) { + let different = { + let mut b = self.url.borrow_mut(); + let different = *b != value; + if different { *b = value; } + different + }; + if different { + self.update_used_text_from_url(); + self.emit().url_changed(); + } + } + + fn used_text<F>(&self, setter: F) where F: FnOnce(&str) { + setter(&*self.used_text.borrow()); + } + + fn set_used_text(&self, value: String) { + let different = { + let mut b = self.used_text.borrow_mut(); + let different = *b != value; + if different { *b = value; } + different + }; + if different { + self.update_generated_password(); + self.emit().used_text_changed(); + } + } + + fn generator_state(&self) -> u8 { + (*self.generator_state.borrow()).into() + } + + fn profile_changed(&self) { + self.update_used_text_from_url(); + } +} + +impl Drop for PasswordMaker +{ + fn drop(&mut self) { + if self.to_helper.send(UiToHelper::Shutdown).is_err() { + eprintln!("Failed to tell worker thread to quit. Might need to kill the process."); + } + if let Some(j) = self.helper_thread.take() { + if j.join().is_err() { + eprintln!("Helper Thread crashed. This should not happen, so please report a bug."); + } + } else { + eprintln!("Somehow the information about the helper thread got lost, so we can't wait for it to quit. Might need to kill the process."); + } + } +} + +impl PasswordMaker{ + fn handle_message_from_helper(&self, message : HelperToUi){ + //unless everything is terribly wrong, we are in the UI thread here. + match message { + HelperToUi::Generated{ password } => { + //println!("Password generated."); + { + *self.generator_state.borrow_mut() = GeneratorState::GenerationCompleted; + *self.generated_password.borrow_mut() = password; + } + self.emit().generator_state_changed(); + self.emit().generated_password_changed(); + } + HelperToUi::GenerationFailed { error } => { + //println!("No password generated due to missing input."); + { + *self.generator_state.borrow_mut() = match error { + GenerationIssue::Settings(SettingsError::InsufficientCharset) => GeneratorState::CharsetError, + GenerationIssue::Input(GenerationError::MissingMasterPassword) => GeneratorState::MissingMasterPassword, + GenerationIssue::Input(GenerationError::MissingTextToUse) => GeneratorState::MissingTextToUse, + }; + self.generated_password.borrow_mut().clear(); + } + self.emit().generator_state_changed(); + self.emit().generated_password_changed(); + }, + HelperToUi::GenerationStarted => { + //println!("Setting password generator state as busy."); + { + *self.generator_state.borrow_mut() = GeneratorState::Busy; + self.generated_password.borrow_mut().clear(); + } + self.emit().generator_state_changed(); + self.emit().generated_password_changed(); + }, + } + } + fn update_used_text_from_url(&self) { + let used_text = { + self.profiles.do_with_current_url_parsing_settings( + |settings| { + //have to convert saved data to corresponding runtime data. + let use_protocol = match (settings.use_protocol, settings.use_undefined_as_protocol_fallback) { + (true, false) => ProtocolUsageMode::Used, + (true, true) => ProtocolUsageMode::UsedWithUndefinedIfEmpty, + (false, _) => ProtocolUsageMode::Ignored, + }; + let url_parsing = UrlParsing::new( + use_protocol, + settings.use_userinfo, + settings.use_subdomains, + settings.use_domain, + settings.use_port_path); + url_parsing.parse(&self.url.borrow()) + } + ) + }; + { *self.used_text.borrow_mut() = used_text.unwrap_or_else(|e| e.to_string()); } + self.emit().used_text_changed(); + self.update_generated_password(); //intentionally unconditional. + } + fn update_generated_password(&self) { + let generation_settings = self.profiles.get_copy_current_generation_settings(); + match generation_settings { + Ok(generation_settings) => { + self.to_helper.send( + UiToHelper::GeneratePassword( + GeneratePasswordTask{ + input: self.used_text.borrow().clone(), + master_password: self.master_password.borrow().clone(), + generation_settings } + ) + ).expect("Helper thread no longer listening. Unrecoverable."); + } + Err(error) => { + {*self.generated_password.borrow_mut() = error.to_string();} + {*self.generator_state.borrow_mut() = GeneratorState::GenerationCompleted;} + self.emit().generator_state_changed(); + self.emit().generated_password_changed(); + } + } + } +} + +#[cfg(test)] +mod passwordmaker_tests { + use std::convert::TryInto; + + use super::*; + #[test] + fn generator_state_reciprocity() { + for i in 0..(GeneratorState::variant_count() as u8) { + let g : GeneratorState = i.try_into().unwrap(); + let j : u8 = g.into(); + assert_eq!(j,i); + } + for i in (GeneratorState::variant_count() as u8)..=255 { + let g : Result<GeneratorState,_> = i.try_into(); + assert!(g.is_err()); + } + } +} diff --git a/rust/src/implementation/passwordmaker/settings.rs b/rust/src/implementation/passwordmaker/settings.rs new file mode 100644 index 0000000..487ffea --- /dev/null +++ b/rust/src/implementation/passwordmaker/settings.rs @@ -0,0 +1,159 @@ +use crate::interface::SettingsTrait; +use crate::implementation::{LoadError, StoreError}; + +use serde::{Serialize, Deserialize}; +use std::default::Default; +use std::cell::RefCell; + +use mockall_double::double; + +#[double] +use crate::interface::SettingsEmitter; + +#[derive(Serialize, Deserialize)] +struct SettingsSaveData { + current_profile_index : usize, + settings : SettingsData +} + +#[derive(Serialize, Deserialize, Clone)] +struct SettingsData{ + clear_generated_password_seconds : Option<u32>, + clear_master_password_seconds : Option<u32>, + hide_generated_password : bool +} + +impl Default for SettingsData { + fn default() -> Self { + SettingsData { + clear_generated_password_seconds : Some(60), + clear_master_password_seconds : Some(300), + hide_generated_password : false + } + } +} + +pub struct Settings { + emit : SettingsEmitter, + data : RefCell<SettingsData> +} + +pub struct SettingLoadResult { + pub settings : Settings, + pub current_profile_index : usize +} + +impl Settings { + /// Loads the data from disk and merges them with the current Settings object. Only use during startup, as it does NOT emit. + pub(super) fn load(&self) -> Result<SettingLoadResult, LoadError>{ + super::super::get_config_folder() + .map(|p| p.join("settings")) + .ok_or(LoadError::Xdg) + .and_then(|p| std::fs::read_to_string(p).map_err(Into::into)) + .and_then(|s| toml::from_str(&s).map_err(Into::into)) + .map(|sd| self.merge_with_loaded_data_no_emit(sd)) + } + pub(super) fn store(&self, current_profile_index : usize) -> Result<(), StoreError>{ + toml::to_string(&SettingsSaveData{current_profile_index, settings : self.data.borrow().clone()}) + .map_err(Into::into) + .and_then(|s| Self::write_serialized_settings_data(&s)) + } + + ///Private. Should only be used during load(). + fn merge_with_loaded_data_no_emit(&self, save_data: SettingsSaveData) -> SettingLoadResult { + SettingLoadResult { + settings : Settings { emit : self.emit.clone(), data : RefCell::new(save_data.settings) }, + current_profile_index : save_data.current_profile_index + } + } + fn write_serialized_settings_data(data : &str) -> Result<(), StoreError> { + super::super::get_config_folder() + .ok_or(StoreError::Xdg) + .and_then(|p| std::fs::create_dir_all(p.clone()).map_err(Into::into).map(|()| p)) + .map(|p| p.join("settings")) + .and_then(|f| std::fs::write(f, data).map_err(Into::into)) + } +} + +impl SettingsTrait for Settings { + fn new(emit: SettingsEmitter) -> Self { + Settings {emit, data : RefCell::new(SettingsData::default())} + } + fn emit(&self) -> &SettingsEmitter{ + &self.emit + } + fn clear_generated_password_seconds(&self) -> Option<u32> { + self.data.borrow().clear_generated_password_seconds + } + fn set_clear_generated_password_seconds(&self, value: Option<u32>) { + let changed = self.data.borrow().clear_generated_password_seconds != value; + self.data.borrow_mut().clear_generated_password_seconds = value; + if changed { + self.emit().clear_generated_password_seconds_changed(); + } + } + fn clear_master_password_seconds(&self) -> Option<u32> { + self.data.borrow().clear_master_password_seconds + } + fn set_clear_master_password_seconds(&self, value: Option<u32>) { + let changed = self.data.borrow().clear_master_password_seconds != value; + self.data.borrow_mut().clear_master_password_seconds = value; + if changed { + self.emit().clear_master_password_seconds_changed(); + } + } + + fn hide_generated_password(&self) -> bool { + self.data.borrow().hide_generated_password + } + + fn set_hide_generated_password(&self, value: bool) { + let changed = { + let mut v = self.data.borrow_mut(); + let changed = v.hide_generated_password != value; + v.hide_generated_password = value; + changed + }; + if changed { + self.emit().hide_generated_password_changed(); + } + } +} + +#[cfg(test)] +mod settings_test{ + use super::*; + + #[test] + fn merge_with_loaded_data_test(){ + let mut emit = SettingsEmitter::new(); + emit.expect_clone().return_once(||{ + let mut e2 = SettingsEmitter::new(); + e2.expect_clone().never(); + e2.expect_clear_generated_password_seconds_changed().never(); + e2.expect_clear_master_password_seconds_changed().never(); + e2.expect_hide_generated_password_changed().never(); + e2 + }).once(); + emit.expect_clear_generated_password_seconds_changed().never(); + emit.expect_clear_master_password_seconds_changed().never(); + emit.expect_hide_generated_password_changed().never(); + let old_settings = Settings { + emit, + data: RefCell::new(SettingsData::default()), + }; + let save_data = SettingsSaveData{ + current_profile_index: 2, + settings: SettingsData { + clear_generated_password_seconds: Some(600), + clear_master_password_seconds: None, + hide_generated_password: true + }, + }; + let result = old_settings.merge_with_loaded_data_no_emit(save_data); + assert_eq!(result.current_profile_index, 2); + assert_eq!(result.settings.data.borrow().clear_generated_password_seconds, Some(600)); + assert_eq!(result.settings.data.borrow().clear_master_password_seconds, None); + assert_eq!(result.settings.data.borrow().hide_generated_password, true); + } +}
\ No newline at end of file diff --git a/rust/src/implementation/passwordmaker/thread_messages.rs b/rust/src/implementation/passwordmaker/thread_messages.rs new file mode 100644 index 0000000..bba3dfa --- /dev/null +++ b/rust/src/implementation/passwordmaker/thread_messages.rs @@ -0,0 +1,41 @@ +use std::sync::Arc; +use passwordmaker_rs::{GenerationError,SettingsError}; +use crate::implementation::profiles::GenerationSettings; + +pub(super) enum HelperToUi { + Generated { + password : String, + }, + GenerationFailed { + error : GenerationIssue + }, + GenerationStarted +} + +pub(super) struct GeneratePasswordTask{ + pub(crate) input : String, + pub(crate) master_password : String, + pub(crate) generation_settings : Arc<GenerationSettings> +} + +pub(super) enum UiToHelper { + Shutdown, + GeneratePassword(GeneratePasswordTask), +} + +pub(super) enum GenerationIssue { + Settings(SettingsError), + Input(GenerationError), +} + +impl From<SettingsError> for GenerationIssue { + fn from(s: SettingsError) -> Self { + GenerationIssue::Settings(s) + } +} + +impl From<GenerationError> for GenerationIssue { + fn from(g: GenerationError) -> Self { + GenerationIssue::Input(g) + } +}
\ No newline at end of file diff --git a/rust/src/implementation/profiles.rs b/rust/src/implementation/profiles.rs new file mode 100644 index 0000000..5d9bbfc --- /dev/null +++ b/rust/src/implementation/profiles.rs @@ -0,0 +1,1127 @@ +use std::convert::{From, Into, TryFrom, TryInto}; +use std::cell::RefCell; +use std::fmt::Display; +use std::sync::Arc; +use serde::{Serialize, Deserialize}; +use mockall_double::double; +#[cfg(test)] +use crate::implementation::pwm_macros::*; + +#[double] +use crate::interface::{ProfilesEmitter, ProfilesList}; +use crate::interface::ProfilesTrait; + +use crate::implementation::{LoadError, StoreError}; + +#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Debug)] +#[cfg_attr(test, derive(EnumVariantCount))] +pub(crate) enum HashAlgorithm { + Md4, + HmacMd4, + Md5, + Md5Version06, + HmacMd5, + HmacMd5Version06, + Sha1, + HmacSha1, + Sha256, + HmacSha256, + Ripemd160, + HmacRipemd160, +} + +impl Display for HashAlgorithm { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + HashAlgorithm::Md4 => write!(f,"MD4"), + HashAlgorithm::HmacMd4 => write!(f, "MD4-HMAC"), + HashAlgorithm::Md5 => write!(f, "MD5"), + HashAlgorithm::Md5Version06 => write!(f, "MD5 PWM Version 0.6"), + HashAlgorithm::HmacMd5 => write!(f, "MD5-HMAC"), + HashAlgorithm::HmacMd5Version06 => write!(f, "MD5-HMAC PWM Version 0.6"), + HashAlgorithm::Sha1 => write!(f,"SHA-1"), + HashAlgorithm::HmacSha1 => write!(f, "SHA-1-HMAC"), + HashAlgorithm::Sha256 => write!(f, "SHA-256"), + HashAlgorithm::HmacSha256 => write!(f, "SHA-256-HMAC"), + HashAlgorithm::Ripemd160 => write!(f, "RIPEMD-160"), + HashAlgorithm::HmacRipemd160 => write!(f, "RIPEMD-160-HMAC"), + } + } +} + +impl From<HashAlgorithm> for u8 { + fn from(h : HashAlgorithm) -> u8 { + h as u8 + } +} +impl TryFrom<u8> for HashAlgorithm { + type Error = (); + fn try_from(i : u8) -> Result<HashAlgorithm,()> { + match i { + 0 => { Ok(HashAlgorithm::Md4) } + 1 => { Ok(HashAlgorithm::HmacMd4) } + 2 => { Ok(HashAlgorithm::Md5) } + 3 => { Ok(HashAlgorithm::Md5Version06) } + 4 => { Ok(HashAlgorithm::HmacMd5) } + 5 => { Ok(HashAlgorithm::HmacMd5Version06) } + 6 => { Ok(HashAlgorithm::Sha1) } + 7 => { Ok(HashAlgorithm::HmacSha1) } + 8 => { Ok(HashAlgorithm::Sha256) } + 9 => { Ok(HashAlgorithm::HmacSha256) } + 10 => {Ok(HashAlgorithm::Ripemd160) } + 11 => {Ok(HashAlgorithm::HmacRipemd160)} + _ => { Err(()) } + } + } +} + +#[derive(Serialize, Deserialize, Clone, Copy, PartialEq)] +#[cfg_attr(test, derive(EnumVariantCount))] +pub(crate) enum LeetLevel { + One, + Two, + Three, + Four, + Five, + Six, + Seven, + Eight, + Nine, +} + +impl From<LeetLevel> for u8 { + fn from(l : LeetLevel) -> u8 { + l as u8 + 1 + } +} +impl TryFrom<u8> for LeetLevel { + type Error = (); + fn try_from(i : u8) -> Result<LeetLevel,()> { + match i { + 1 => { Ok(LeetLevel::One) } + 2 => { Ok(LeetLevel::Two) } + 3 => { Ok(LeetLevel::Three) } + 4 => { Ok(LeetLevel::Four) } + 5 => { Ok(LeetLevel::Five) } + 6 => { Ok(LeetLevel::Six) } + 7 => { Ok(LeetLevel::Seven) } + 8 => { Ok(LeetLevel::Eight) } + 9 => { Ok(LeetLevel::Nine) } + _ => { Err(()) } + } + } +} + + +#[derive(Serialize, Deserialize, Clone, PartialEq)] +#[serde(tag = "UseLeet")] +pub(crate) enum UseLeetWhenGenerating { + NotAtAll, + Before { + level : LeetLevel, + }, + After { + level : LeetLevel, + }, + BeforeAndAfter { + level : LeetLevel, + }, +} + +impl UseLeetWhenGenerating { + pub(crate) fn get_leet_level(&self) -> Option<LeetLevel> { + match self { + UseLeetWhenGenerating::NotAtAll => { None } + UseLeetWhenGenerating::Before { level } + | UseLeetWhenGenerating::After { level } + | UseLeetWhenGenerating::BeforeAndAfter { level } => { Some(*level) } + } + } + pub(crate) fn set_leet_level(&mut self, lvl : LeetLevel) -> Result<(),()> { + match self { + UseLeetWhenGenerating::NotAtAll => { Err(()) } + UseLeetWhenGenerating::Before { level } + | UseLeetWhenGenerating::After { level } + | UseLeetWhenGenerating::BeforeAndAfter { level} => { *level = lvl; Ok(()) } + } + } +} + +#[allow(clippy::struct_excessive_bools)] +#[derive(Serialize, Deserialize, Clone)] +pub(super) struct UrlParsingSettings { + pub(super) use_protocol : bool, + pub(super) use_userinfo : bool, + pub(super) use_subdomains : bool, + pub(super) use_domain : bool, + pub(super) use_port_path : bool, + pub(super) use_undefined_as_protocol_fallback : bool +} + +impl std::default::Default for UrlParsingSettings { + fn default() -> Self { + Self { + use_protocol: false, + use_userinfo : false, + use_subdomains: false, + use_domain: true, + use_port_path: false , + use_undefined_as_protocol_fallback: true + } + } +} + +#[derive(Serialize, Deserialize, Clone)] +pub(crate) struct GenerationSettings { + pub(crate) password_length : u32, + pub(crate) username : String, + pub(crate) modifier : String, + pub(crate) characters : String, + pub(crate) prefix : String, + pub(crate) suffix : String, + pub(crate) hash_algorithm : HashAlgorithm, + pub(crate) leet : UseLeetWhenGenerating, +} + +impl std::default::Default for GenerationSettings { + fn default() -> Self { + Self { + leet : UseLeetWhenGenerating::NotAtAll, + hash_algorithm : HashAlgorithm::Md5, + password_length : 8, + username : String::new(), + modifier : String::new(), + characters : String::from( + r#"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789`~!@#$%^&*()_-+={}|[]\:";'<>?,./"# + ), + prefix : String::new(), + suffix : String::new(), + } + } +} + +#[derive(Serialize, Deserialize, Clone, Default)] +struct ProfileSettings { + generation_settings : Arc<GenerationSettings>, + url_parsing_settings : UrlParsingSettings, +} + +#[derive(Serialize, Deserialize)] +struct Profile { + name : String, + settings : ProfileSettings, +} + +#[derive(Serialize, Deserialize)] +struct StoredProfiles { + profiles : Vec<Profile>, +} + +impl std::default::Default for StoredProfiles { + fn default() -> Self { + Self { + profiles : vec![Profile { + name : String::from("Default"), + settings : ProfileSettings::default() + }] + } + } +} + +impl StoredProfiles { + fn load() -> Result<Self, LoadError> { + super::get_config_folder() + .map(|p| p.join("profiles")) + .ok_or(LoadError::Xdg) + .and_then(|p| std::fs::read_to_string(p).map_err(Into::into)) + .and_then(|s| toml::from_str(&s).map_err(Into::into)) + } + + fn store(&self) -> Result<(), StoreError> { + toml::to_string(self).map_err(Into::into) + .and_then(|s| Self::write_serialized_profile_data(&s)) + } + fn write_serialized_profile_data(data : &str) -> Result<(), StoreError> { + super::get_config_folder() + .ok_or(StoreError::Xdg) + .and_then(|p| std::fs::create_dir_all(p.clone()).map_err(Into::into).map(|()| p)) + .map(|p| p.join("profiles")) + .and_then(|f| std::fs::write(f, data).map_err(Into::into)) + } +} + +pub struct Profiles { + emit : ProfilesEmitter, + model : ProfilesList, + + data : RefCell<StoredProfiles>, + + current_profile_idx : RefCell<usize>, +} + +#[derive(Clone, Debug)] +pub(crate) enum ProfileAccessError { + CurrentProfileOutOfBounds +} +impl Display for ProfileAccessError{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ProfileAccessError::CurrentProfileOutOfBounds => write!(f, "error reading current profile, please reselect profile") + } + } +} +impl std::error::Error for ProfileAccessError{} + +impl Profiles { + fn notify_current_profile_changed(&self) { + self.emit().current_profile_changed(); + self.emit().current_profile_name_changed(); + } + + fn model(&self) -> &ProfilesList { + #[cfg(debug_assertions)] + assert!(!self.is_any_borrowed()); + &self.model + } + + pub(super) fn do_with_current_url_parsing_settings<F, R>(&self, operation : F) -> Result<R, ProfileAccessError> + where F : FnOnce(&UrlParsingSettings) -> R + { + self.data.borrow().profiles.get(*self.current_profile_idx.borrow()) + .map(|p| &p.settings.url_parsing_settings).map(operation) + .ok_or(ProfileAccessError::CurrentProfileOutOfBounds) + } + + pub(crate) fn get_copy_current_generation_settings(&self) -> Result<Arc<GenerationSettings>, ProfileAccessError> { + self.data.borrow().profiles.get(*self.current_profile_idx.borrow()) + .map(|p| p.settings.generation_settings.clone()) + .ok_or(ProfileAccessError::CurrentProfileOutOfBounds) + } + + #[cfg(debug_assertions)] + fn is_any_borrowed(&self) -> bool { + self.data.try_borrow_mut().is_err() || + self.current_profile_idx.try_borrow_mut().is_err() + } +} + +impl ProfilesTrait for Profiles { + fn new(emit : ProfilesEmitter, model : ProfilesList) -> Self { + let data = RefCell::new(StoredProfiles::load().unwrap_or_default()); + Profiles { emit, model, data, current_profile_idx : RefCell::new(0) } + } + fn emit(&self) -> &ProfilesEmitter { + #[cfg(debug_assertions)] + assert!(!self.is_any_borrowed()); + &self.emit + } + + fn row_count(&self) -> usize { + self.data.borrow().profiles.len() + } + fn insert_rows(&self, row : usize, count : usize) -> bool { + let max_index = self.row_count(); + let valid_index = row <= self.data.borrow().profiles.len(); + if valid_index { + self.model().begin_insert_rows(row, row + count - 1); + let new_profiles = (0..count) + .map(|c| format!("New Profile {}", max_index + c)) + .map(|name| Profile { name, settings : ProfileSettings::default() }); + self.data.borrow_mut().profiles.splice(row..row,new_profiles); + self.model().end_insert_rows(); + //if the current profile is at or beyond row, we need to update it, so it still points + //to the same profile + if *self.current_profile_idx.borrow() >= row { + *self.current_profile_idx.borrow_mut() += count; + //no need to notify about field changes - those are still the same. Only the + //internal index changed. + self.emit().current_profile_changed(); + } + } + valid_index + } + fn remove_rows(&self, row : usize, count : usize) -> bool { + let valid_index = row + count <= self.row_count(); + let at_least_one_left = self.row_count() > count; + let operation_allowed = valid_index && at_least_one_left; + if operation_allowed { + self.model().begin_remove_rows(row, row + count - 1); + self.data.borrow_mut().profiles.drain(row..(row+count)); + //we have to update the current profile index before we complete the row removal. + //this is to ensure the index is not going invalid. + //here we can have a destructive operation: the current index might have been removed. + //in that case set it to 0. + let current_idx = *self.current_profile_idx.borrow(); + if row + count <= current_idx { + *self.current_profile_idx.borrow_mut() -= count; + //same as in the insert_rows case here: The field values are still the same, only + //the index changed. + self.emit().current_profile_changed(); + } + else if row <= current_idx { + *self.current_profile_idx.borrow_mut() = 0; + self.notify_current_profile_changed(); + } + self.model().end_remove_rows(); + } + operation_allowed + } + + fn characters<F : FnOnce(&str)>(&self, index : usize, setter : F) { + setter(self.data.borrow().profiles.get(index) + .map(|p| &*p.settings.generation_settings.characters) + .unwrap_or_default()); + } + fn set_characters(&self, index : usize, val : String) -> bool { + if let Some(chars) = self.data.borrow_mut().profiles.get_mut(index) + .map(|p| &mut Arc::make_mut(&mut p.settings.generation_settings).characters) + .filter(|c| **c != val) { + *chars = val; + true + } else { + false + } + } + fn hash_algorithm(&self, index : usize) -> u8 { + self.data.borrow().profiles.get(index) + .map(|p| p.settings.generation_settings.hash_algorithm.into()) + .unwrap_or_default() + } + fn set_hash_algorithm(&self, index : usize, val : u8) -> bool { + let hash = val.try_into().ok(); + let mut borrow = self.data.borrow_mut(); + let profile = borrow.profiles.get_mut(index); + if let Some((p, h)) = profile.zip(hash) + .map(|(p, h)| (&mut Arc::make_mut(&mut p.settings.generation_settings).hash_algorithm, h)) + .filter(|(p, h)| *p != h) { + *p = h; + true + } else { + false + } + } + fn leet_level(&self, index : usize) -> u8 { + self.data.borrow().profiles.get(index) + .and_then(|p| p.settings.generation_settings.leet.get_leet_level()) + .map(Into::into) + .unwrap_or_default() + } + fn set_leet_level(&self, index : usize, level : u8) -> bool { + let level = level.try_into().ok(); + let mut borrow = self.data.borrow_mut(); + let profile = borrow.profiles.get_mut(index); + if let Some((p, l)) = profile.zip(level) + .map(|(p,l)| (&mut Arc::make_mut(&mut p.settings.generation_settings).leet, l)) + .filter(|(p,l)| p.get_leet_level() != Some(*l)) { + p.set_leet_level(l).is_ok() + } else { + false + } + } + fn modifier<F : FnOnce(&str)>(&self, index : usize, setter : F) { + setter(self.data.borrow().profiles.get(index) + .map(|p| &*p.settings.generation_settings.modifier) + .unwrap_or_default()); + } + fn set_modifier(&self, index : usize, val : String) -> bool { + if let Some(m) = self.data.borrow_mut().profiles.get_mut(index) + .map(|p| &mut Arc::make_mut(&mut p.settings.generation_settings).modifier) + .filter(|m| **m != val) { + *m = val; + true + } else { + false + } + } + fn name<F : FnOnce(&str)>(&self, index : usize, setter : F) { + setter(self.data.borrow().profiles.get(index) + .map_or("profile index invalid",|p| &*p.name)); + } + fn set_name(&self, index : usize, val : String) -> bool { + let changed = { + if let Some(n) = self.data.borrow_mut().profiles.get_mut(index) + .map(|p| &mut p.name) + .filter(|n| **n != val) { + *n = val; + true + } else { + false + } + }; + if changed && index == *self.current_profile_idx.borrow() { + self.emit().current_profile_name_changed(); + } + changed + } + fn password_length(&self, index : usize) -> u32 { + self.data.borrow().profiles.get(index) + .map(|p| p.settings.generation_settings.password_length) + .unwrap_or_default() + } + fn set_password_length(&self, index : usize, len : u32) -> bool { + if let Some(pl) = self.data.borrow_mut().profiles.get_mut(index) + .map(|p| &mut Arc::make_mut(&mut p.settings.generation_settings).password_length) + .filter(|pl| **pl != len) { + *pl = len; + true + } else { + false + } + } + fn prefix<F : FnOnce(&str)>(&self, index : usize, setter : F) { + setter(self.data.borrow().profiles.get(index) + .map(|p| &*p.settings.generation_settings.prefix) + .unwrap_or_default()); + } + fn set_prefix(&self, index : usize, pref : String) -> bool { + if let Some(p) = self.data.borrow_mut().profiles.get_mut(index) + .map(|p| &mut Arc::make_mut(&mut p.settings.generation_settings).prefix) + .filter(|p| **p != pref) { + *p = pref; + true + } else { + false + } + } + fn suffix<F : FnOnce(&str)>(&self, index : usize, setter : F) { + setter(self.data.borrow_mut().profiles.get(index) + .map(|p| &*p.settings.generation_settings.suffix) + .unwrap_or_default()); + } + fn set_suffix(&self, index : usize, suf : String) -> bool { + if let Some(s) = self.data.borrow_mut().profiles.get_mut(index) + .map(|p| &mut Arc::make_mut(&mut p.settings.generation_settings).suffix) + .filter(|s| **s != suf) { + *s=suf; + true + } else { + false + } + } + fn use_domain(&self, index : usize) -> bool { + self.data.borrow().profiles.get(index) + .map_or(true,|p| p.settings.url_parsing_settings.use_domain) + } + fn set_use_domain(&self, index : usize, val : bool) -> bool { + if let Some(d) = self.data.borrow_mut().profiles.get_mut(index) + .map(|p| &mut p.settings.url_parsing_settings.use_domain) + .filter(|d| **d != val) { + *d = val; + true + } else { + false + } + } + fn use_leet(&self, index : usize) -> u8 { + self.data.borrow().profiles.get(index) + .map(|p| match p.settings.generation_settings.leet { + UseLeetWhenGenerating::NotAtAll => { 0 } + UseLeetWhenGenerating::Before{..} => { 1 } + UseLeetWhenGenerating::After {..} => { 2 } + UseLeetWhenGenerating::BeforeAndAfter{..} => { 3 } + }) + .unwrap_or_default() + } + fn set_use_leet(&self, index : usize, l : u8) -> bool { + let mut borrow = self.data.borrow_mut(); + let profile = borrow.profiles.get_mut(index); + let level = |profile : Option<&Profile>| { + profile.as_ref().and_then(|p| p.settings.generation_settings.leet.get_leet_level()) + .unwrap_or(LeetLevel::One) + }; + let use_leet = match l { + 0 => { Some(UseLeetWhenGenerating::NotAtAll) } + 1 => { Some(UseLeetWhenGenerating::Before { level : level(profile.as_deref()) }) } + 2 => { Some(UseLeetWhenGenerating::After { level : level(profile.as_deref()) }) } + 3 => { Some(UseLeetWhenGenerating::BeforeAndAfter{ level : level(profile.as_deref()) }) } + _ => { None } + }; + if let Some((p, l)) = profile.zip(use_leet) + .map(|(p,l)| (&mut Arc::make_mut(&mut p.settings.generation_settings).leet, l)) + .filter(|(p,l)| *p != l) { + *p = l; + true + } else { + false + } + } + fn use_port_path(&self, index : usize) -> bool { + self.data.borrow().profiles.get(index) + .map(|p| p.settings.url_parsing_settings.use_port_path) + .unwrap_or_default() + } + fn set_use_port_path(&self, index : usize, val : bool) -> bool { + if let Some(pp) = self.data.borrow_mut().profiles.get_mut(index) + .map(|p| &mut p.settings.url_parsing_settings.use_port_path) + .filter(|pp| **pp != val) { + *pp = val; + true + } else { + false + } + } + fn use_protocol(&self, index : usize) -> bool { + self.data.borrow().profiles.get(index) + .map(|p| p.settings.url_parsing_settings.use_protocol) + .unwrap_or_default() + } + fn set_use_protocol(&self, index : usize, val : bool) -> bool { + if let Some(pr) = self.data.borrow_mut().profiles.get_mut(index) + .map(|p| &mut p.settings.url_parsing_settings.use_protocol) + .filter(|pr| **pr != val) { + *pr = val; + true + } else { + false + } + } + fn use_subdomains(&self, index : usize) -> bool { + self.data.borrow().profiles.get(index) + .map(|p| p.settings.url_parsing_settings.use_subdomains) + .unwrap_or_default() + } + fn set_use_subdomains(&self, index : usize, val : bool) -> bool { + if let Some(sd) = self.data.borrow_mut().profiles.get_mut(index) + .map(|p| &mut p.settings.url_parsing_settings.use_subdomains) + .filter(|sd| **sd != val) { + *sd = val; + true + } else { + false + } + } + fn username<F : FnOnce(&str)>(&self, index : usize, setter : F) { + setter(self.data.borrow().profiles.get(index) + .map(|p| &*p.settings.generation_settings.username) + .unwrap_or_default()); + } + fn set_username(&self, index : usize, name : String) -> bool { + if let Some(u) = self.data.borrow_mut().profiles.get_mut(index) + .map(|p| &mut Arc::make_mut(&mut p.settings.generation_settings).username) + .filter(|u| **u != name) { + *u = name; + true + } else { + false + } + } + + #[allow(clippy::cast_possible_truncation)] + fn current_profile(&self) -> u32 { + *self.current_profile_idx.borrow() as u32 + } + fn set_current_profile(&self, value : u32) { + let valid_index = self.row_count() > value as usize; + let does_change = *self.current_profile_idx.borrow() != value as usize; + if valid_index && does_change { + *self.current_profile_idx.borrow_mut() = value as usize; + self.notify_current_profile_changed(); + } + } + fn current_profile_name<F : FnOnce(&str)>(&self, setter : F) { + self.name(*self.current_profile_idx.borrow(), setter); + } + + fn store(&self) -> bool { + self.data.borrow().store() + .map_err(|e| println!("{}", e)) + .is_ok() + } + + fn use_undefined_as_protocol_fallback(&self, index: usize) -> bool { + self.data.borrow().profiles.get(index) + .map(|p| p.settings.url_parsing_settings.use_undefined_as_protocol_fallback) + .unwrap_or_default() + } + + fn set_use_undefined_as_protocol_fallback(&self, index: usize, val: bool) -> bool { + if let Some(sd) = self.data.borrow_mut().profiles.get_mut(index) + .map(|p| &mut p.settings.url_parsing_settings.use_undefined_as_protocol_fallback) + .filter(|sd| **sd != val) { + *sd = val; + true + } else { + false + } + } + + fn use_user_info(&self, index: usize) -> bool { + self.data.borrow().profiles.get(index) + .map(|p| p.settings.url_parsing_settings.use_userinfo) + .unwrap_or_default() + } + + fn set_use_user_info(&self, index: usize, val: bool) -> bool { + if let Some(sd) = self.data.borrow_mut().profiles.get_mut(index) + .map(|p| &mut p.settings.url_parsing_settings.use_userinfo) + .filter(|sd| **sd != val) { + *sd = val; + true + } else { + false + } + } +} + +#[cfg(test)] +mod profiles_tests { + use super::*; + fn get_test_profiles_three_entries(emit : ProfilesEmitter, model : ProfilesList) -> Profiles { + Profiles { + emit, + model, + data : RefCell::new( StoredProfiles { + profiles : vec![ + Profile { + name : String::from("First"), + settings : ProfileSettings::default(), + }, + Profile { + name : String::from("Second"), + settings : ProfileSettings::default(), + }, + Profile { + name : String::from("Third"), + settings : ProfileSettings::default(), + }, + ], + }), + current_profile_idx : RefCell::new(1), + } + } + #[test] + fn hash_algo_reciprocicity_test() { + for i in 0..(HashAlgorithm::variant_count() as u8) { + let x : HashAlgorithm = i.try_into().unwrap(); + let y = x.into(); + assert_eq!(i,y); + } + let z: Result<HashAlgorithm,_> = (HashAlgorithm::variant_count() as u8).try_into(); + assert!(z.is_err()); + } + #[test] + fn leet_level_reciprocicity_test() { + for i in 1..=(LeetLevel::variant_count() as u8) { + let x : LeetLevel = i.try_into().unwrap(); + let y = x.into(); + assert_eq!(i,y); + } + let z : Result<LeetLevel,_> = (1+LeetLevel::variant_count() as u8).try_into(); + assert!(z.is_err()); + let a : Result<LeetLevel,_> = 0.try_into(); + assert!(a.is_err()); + } + #[test] + fn set_name_test() { + let (emit_nothing, model_doesnt_emit) = expect_no_emission(); + let profiles_assert_on_emit = get_test_profiles_three_entries(emit_nothing, model_doesnt_emit); + //profiles_assert_on_emit has profile 1 set as active. We can therefore modify the name of 0 (or 2) without emission. + let should_change = profiles_assert_on_emit.set_name(0, "SomethingIJustMadeUp".into()); + assert!(should_change); + profiles_assert_on_emit.name(0, |x| {assert_eq!(x, "SomethingIJustMadeUp")}); + } + #[test] + fn set_current_profile_test() { + { + //setting to same index should not do anything. + let (emit_nothing, model_doesnt_emit) = expect_no_emission(); + let profiles_assert_on_emit = get_test_profiles_three_entries(emit_nothing, model_doesnt_emit); + profiles_assert_on_emit.set_current_profile(profiles_assert_on_emit.current_profile()); + } + { + //setting to different index should emit both name and index changed. + let mut emit = ProfilesEmitter::new(); + emit.expect_clone().never(); + emit.expect_current_profile_changed().return_once(||{()}).once(); + emit.expect_current_profile_name_changed().return_once(||{()}).once(); + emit.expect_new_data_ready().never(); + let mut list = ProfilesList::new(); + list.expect_layout_about_to_be_changed().never(); + list.expect_layout_changed().never(); + list.expect_data_changed().never(); + list.expect_begin_reset_model().never(); + list.expect_end_reset_model().never(); + list.expect_begin_insert_rows().never(); + list.expect_end_insert_rows().never(); + list.expect_begin_move_rows().never(); + list.expect_end_move_rows().never(); + list.expect_begin_remove_rows().never(); + list.expect_end_remove_rows().never(); + let profiles_wants_one_name_change = get_test_profiles_three_entries(emit, list); + let previous_profile = profiles_wants_one_name_change.current_profile(); + profiles_wants_one_name_change.set_current_profile(previous_profile + 1); + assert_eq!(previous_profile + 1, profiles_wants_one_name_change.current_profile()); + } + { + //Last, but not least, even if the _name_ of the profiles is the same, we WANT to emit current_profile_name_changed. + //This is because the UI is also functioning as Controller, so every profile change (even if it's the same name) should refresh the + //corresponding UI. + + let mut emit = ProfilesEmitter::new(); + emit.expect_clone().never(); + emit.expect_current_profile_changed().return_once(||{()}).once(); + emit.expect_current_profile_name_changed().return_once(||{()}).once(); + emit.expect_new_data_ready().never(); + let mut list = ProfilesList::new(); + list.expect_layout_about_to_be_changed().never(); + list.expect_layout_changed().never(); + list.expect_data_changed().never(); + list.expect_begin_reset_model().never(); + list.expect_end_reset_model().never(); + list.expect_begin_insert_rows().never(); + list.expect_end_insert_rows().never(); + list.expect_begin_move_rows().never(); + list.expect_end_move_rows().never(); + list.expect_begin_remove_rows().never(); + list.expect_end_remove_rows().never(); + let p = Profiles { + emit, + model :list, + data : RefCell::new( StoredProfiles { + profiles : vec![ + Profile { + name : String::from("Twin"), + settings : ProfileSettings::default(), + }, + Profile { + name : String::from("Twin"), + settings : ProfileSettings::default(), + }, + ], + }), + current_profile_idx : RefCell::new(1), + }; + p.set_current_profile(0); + assert_eq!(p.current_profile(), 0); + } + } + #[test] + fn current_profile_name_test() { + //let's check if setting the same value we already have emits. Hopefully not. Set the index 1 profile's name to "Second" + { + let (emit_nothing, model_doesnt_emit) = expect_no_emission(); + let profiles_assert_on_emit = get_test_profiles_three_entries(emit_nothing, model_doesnt_emit); + let should_not_change = profiles_assert_on_emit.set_name(1,"Second".into()); + assert!(!should_not_change); + } + + let mut emit = ProfilesEmitter::new(); + emit.expect_clone().never(); + emit.expect_current_profile_changed().never(); + emit.expect_current_profile_name_changed().return_once(||{()}).once(); + emit.expect_new_data_ready().never(); + let mut list = ProfilesList::new(); + list.expect_layout_about_to_be_changed().never(); + list.expect_layout_changed().never(); + list.expect_data_changed().never(); + list.expect_begin_reset_model().never(); + list.expect_end_reset_model().never(); + list.expect_begin_insert_rows().never(); + list.expect_end_insert_rows().never(); + list.expect_begin_move_rows().never(); + list.expect_end_move_rows().never(); + list.expect_begin_remove_rows().never(); + list.expect_end_remove_rows().never(); + let profiles_wants_one_name_change = get_test_profiles_three_entries(emit, list); + let should_change2 = profiles_wants_one_name_change.set_name(1, "Agadlagugu".into()); + assert!(should_change2); + profiles_wants_one_name_change.name(1, |x| {assert_eq!(x, "Agadlagugu")}); + } + #[test] + fn insert_rows_before_current_test() { + //if we add a row before the current, only, current_profile should emit. No need to emit profile_name, as the data is still the same. + let mut emit = ProfilesEmitter::new(); + emit.expect_clone().never(); + emit.expect_current_profile_changed().return_once(||{()}).once(); + emit.expect_current_profile_name_changed().never(); + emit.expect_new_data_ready().never(); + let mut list = ProfilesList::new(); + list.expect_layout_about_to_be_changed().never(); + list.expect_layout_changed().never(); + list.expect_data_changed().never(); + list.expect_begin_reset_model().never(); + list.expect_end_reset_model().never(); + list.expect_begin_insert_rows().return_once(|_,_|{()}).once(); + list.expect_end_insert_rows().return_once(||{()}).once(); + list.expect_begin_move_rows().never(); + list.expect_end_move_rows().never(); + list.expect_begin_remove_rows().never(); + list.expect_end_remove_rows().never(); + //must make sure that index changes, but data remains same + let profile_tests_insert_before_current = get_test_profiles_three_entries(emit, list); + let get_current_name_copy = || { let mut res = String::new(); profile_tests_insert_before_current.current_profile_name(|x| res = String::from(x)); res}; + let old_name = get_current_name_copy(); + let old_index = profile_tests_insert_before_current.current_profile(); + let worked = profile_tests_insert_before_current.insert_rows(1, 1); + assert!(worked); + let new_name = get_current_name_copy(); + let new_index = profile_tests_insert_before_current.current_profile(); + assert_eq!(old_name, new_name); + assert_eq!(new_index, old_index +1); + } + #[test] + fn insert_rows_after_current_test() { + //if we add a row after current, no signals other than insert_rows related ones should emit: + //if we add a row before the current, only, current_profile should emit. No need to emit profile_name, as the data is still the same. + let mut emit = ProfilesEmitter::new(); + emit.expect_clone().never(); + emit.expect_current_profile_changed().never(); + emit.expect_current_profile_name_changed().never(); + emit.expect_new_data_ready().never(); + let mut list = ProfilesList::new(); + list.expect_layout_about_to_be_changed().never(); + list.expect_layout_changed().never(); + list.expect_data_changed().never(); + list.expect_begin_reset_model().never(); + list.expect_end_reset_model().never(); + list.expect_begin_insert_rows().return_once(|_,_|{()}).once(); + list.expect_end_insert_rows().return_once(||{()}).once(); + list.expect_begin_move_rows().never(); + list.expect_end_move_rows().never(); + list.expect_begin_remove_rows().never(); + list.expect_end_remove_rows().never(); + //must make sure that index changes, but data remains same + let profile_tests_insert_before_current = get_test_profiles_three_entries(emit, list); + let get_current_name_copy = || { let mut res = String::new(); profile_tests_insert_before_current.current_profile_name(|x| res = String::from(x)); res}; + let old_name = get_current_name_copy(); + let old_index = profile_tests_insert_before_current.current_profile(); + let worked = profile_tests_insert_before_current.insert_rows(3, 1); + assert!(worked); + let new_name = get_current_name_copy(); + let new_index = profile_tests_insert_before_current.current_profile(); + assert_eq!(old_name, new_name); + assert_eq!(new_index, old_index); + } + #[test] + fn remove_rows_before_current_test() { + //if we remove a row before the current, only current_profile should emit. No need to emit profile_name, as the data is still the same. + let mut emit = ProfilesEmitter::new(); + emit.expect_clone().never(); + emit.expect_current_profile_changed().return_once(||{()}).once(); + emit.expect_current_profile_name_changed().never(); + emit.expect_new_data_ready().never(); + let mut list = ProfilesList::new(); + list.expect_layout_about_to_be_changed().never(); + list.expect_layout_changed().never(); + list.expect_data_changed().never(); + list.expect_begin_reset_model().never(); + list.expect_end_reset_model().never(); + list.expect_begin_insert_rows().never(); + list.expect_end_insert_rows().never(); + list.expect_begin_move_rows().never(); + list.expect_end_move_rows().never(); + list.expect_begin_remove_rows().return_once(|_,_|{()}).once(); + list.expect_end_remove_rows().return_once(||{()}).once(); + //must make sure that index changes, but data remains same + let profile_tests_remove_before_current = get_test_profiles_three_entries(emit, list); + let get_current_name_copy = || { let mut res = String::new(); profile_tests_remove_before_current.current_profile_name(|x| res = String::from(x)); res}; + let old_name = get_current_name_copy(); + let old_index = profile_tests_remove_before_current.current_profile(); + let worked = profile_tests_remove_before_current.remove_rows(0, 1); + assert!(worked); + let new_name = get_current_name_copy(); + let new_index = profile_tests_remove_before_current.current_profile(); + assert_eq!(old_name, new_name); + assert_eq!(new_index, old_index - 1); + } + #[test] + fn remove_rows_containign_current_test() { + //if we remove the current row, both, index and name will change and index should be zero. + let mut emit = ProfilesEmitter::new(); + emit.expect_clone().never(); + emit.expect_current_profile_changed().return_once(||{()}).once(); + emit.expect_current_profile_name_changed().return_once(||{()}).once(); + emit.expect_new_data_ready().never(); + let mut list = ProfilesList::new(); + list.expect_layout_about_to_be_changed().never(); + list.expect_layout_changed().never(); + list.expect_data_changed().never(); + list.expect_begin_reset_model().never(); + list.expect_end_reset_model().never(); + list.expect_begin_insert_rows().never(); + list.expect_end_insert_rows().never(); + list.expect_begin_move_rows().never(); + list.expect_end_move_rows().never(); + list.expect_begin_remove_rows().return_once(|_,_|{()}).once(); + list.expect_end_remove_rows().return_once(||{()}).once(); + //must make sure that index changes, but data remains same + let profile_tests_remove_current = get_test_profiles_three_entries(emit, list); + let get_current_name_copy = || { let mut res = String::new(); profile_tests_remove_current.current_profile_name(|x| res = String::from(x)); res}; + let old_name = get_current_name_copy(); + let old_index = profile_tests_remove_current.current_profile(); + let worked = profile_tests_remove_current.remove_rows(1, 1); + assert!(worked); + let new_name = get_current_name_copy(); + let new_index = profile_tests_remove_current.current_profile(); + assert_ne!(old_name, new_name); + assert_eq!(new_index, 0); + assert_ne!(new_index, old_index); + } + #[test] + fn remove_rows_after_current_test() { + //only remove rows emit, no profile related emit + let mut emit = ProfilesEmitter::new(); + emit.expect_clone().never(); + emit.expect_current_profile_changed().never(); + emit.expect_current_profile_name_changed().never(); + emit.expect_new_data_ready().never(); + let mut list = ProfilesList::new(); + list.expect_layout_about_to_be_changed().never(); + list.expect_layout_changed().never(); + list.expect_data_changed().never(); + list.expect_begin_reset_model().never(); + list.expect_end_reset_model().never(); + list.expect_begin_insert_rows().never(); + list.expect_end_insert_rows().never(); + list.expect_begin_move_rows().never(); + list.expect_end_move_rows().never(); + list.expect_begin_remove_rows().return_once(|_,_|{()}).once(); + list.expect_end_remove_rows().return_once(||{()}).once(); + //must make sure that index changes, but data remains same + let profile_tests_remove_after_current = get_test_profiles_three_entries(emit, list); + let get_current_name_copy = || { let mut res = String::new(); profile_tests_remove_after_current.current_profile_name(|x| res = String::from(x)); res}; + let old_name = get_current_name_copy(); + let old_index = profile_tests_remove_after_current.current_profile(); + let worked = profile_tests_remove_after_current.remove_rows(2, 1); + assert!(worked); + let new_name = get_current_name_copy(); + let new_index = profile_tests_remove_after_current.current_profile(); + assert_eq!(old_name, new_name); + assert_eq!(new_index, old_index); + } + // Leet level needs its own test, as setting leet level depends on use_leet. + #[test] + fn set_leet_level_test() { + let (emit, model) = expect_no_emission(); + //we're not going through the new() method, to avoid touching the filesystem. + let profiles = get_test_profiles_three_entries(emit, model); + + //remember the old values in other rows to confimr they aren't touched + let oldvals = [profiles.leet_level(0), profiles.leet_level(2)]; + + //make sure that leet isn't enabled. + assert_eq!(profiles.use_leet(1), u8::default()); + //make sure we are changing something. + assert_ne!(profiles.leet_level(1), 2); + //assure that nothing happens if we try to change leet level with leet disabled. + let changed = profiles.set_leet_level(1,2); + assert!(!changed); + assert_eq!(profiles.use_leet(1), u8::default()); + + //enable leet. + let changed = profiles.set_use_leet(1,1); + assert!(changed); + //make sure we are changing something. + assert_ne!(profiles.leet_level(1), 2); + //actually change something: + let changed = profiles.set_leet_level(1,2); + assert!(changed); + assert_eq!(profiles.leet_level(1), 2); + //check that other values are unaffected + assert_eq!(profiles.leet_level(0), oldvals[0]); + assert_eq!(profiles.leet_level(2), oldvals[1]); + //IMPORTANT: check that assigning the same value returns not-changed. + let changed = profiles.set_leet_level(1,2); + assert!(!changed); + //disable leet again and make sure leet_level returns 0 again. + let changed = profiles.set_use_leet(1,0); + assert!(changed); + assert_eq!(profiles.leet_level(1), u8::default()); + } + + // The remaining properties should (at the time of writing) not emit. + // This means we can use one and the same test for all of them :-) + // Except that complex types have a different getter syntax... + // Oh well. + fn expect_no_emission() -> (ProfilesEmitter, ProfilesList) { + let mut emit = ProfilesEmitter::new(); + emit.expect_clone().never(); + emit.expect_current_profile_changed().never(); + emit.expect_current_profile_name_changed().never(); + emit.expect_new_data_ready().never(); + let mut list = ProfilesList::new(); + list.expect_layout_about_to_be_changed().never(); + list.expect_layout_changed().never(); + list.expect_data_changed().never(); + list.expect_begin_reset_model().never(); + list.expect_end_reset_model().never(); + list.expect_begin_insert_rows().never(); + list.expect_end_insert_rows().never(); + list.expect_begin_move_rows().never(); + list.expect_end_move_rows().never(); + list.expect_begin_remove_rows().never(); + list.expect_end_remove_rows().never(); + (emit, list) + } + macro_rules! simple_setter_tests { + ( $( $test_name:ident, $setter:ident, $getter:ident, $val:literal ),* ) => { + $(#[test] + fn $test_name() { + let (emit, model) = expect_no_emission(); + //we're not going through the new() method, to avoid touching the filesystem. + let profiles = get_test_profiles_three_entries(emit, model); + + //remember the old values in other rows to confimr they aren't touched + let oldvals = [profiles.$getter(0), profiles.$getter(2)]; + + //make sure we are changing something. + assert_ne!(profiles.$getter(1), $val); + //assure that something changed + let changed = profiles.$setter(1,$val); + assert!(changed); + assert_eq!(profiles.$getter(1), $val); + //check that other values are unaffected + assert_eq!(profiles.$getter(0), oldvals[0]); + assert_eq!(profiles.$getter(2), oldvals[1]); + //IMPORTANT: check that assigning the same value returns not-changed. + let changed = profiles.$setter(1,$val); + assert!(!changed); + })* + }; + } + macro_rules! string_setter_tests { + ( $( $test_name:ident, $setter:ident, $getter:ident, $val:literal ),* ) => { + $(#[test] + fn $test_name() { + let (emit, model) = expect_no_emission(); + //we're not going through the new() method, to avoid touching the filesystem. + let profiles = get_test_profiles_three_entries(emit, model); + + //remember the old values in other rows to confirm they aren't touched. + let mut oldvals = [String::new(), String::new()]; + profiles.$getter(0, |x| { oldvals[0] = String::from(x); }); + profiles.$getter(2, |x| { oldvals[1] = String::from(x); }); + //make sure we are changing something. + profiles.$getter(1, |x| { assert_ne!(x, $val); }); + //assure that something changed: + let changed = profiles.$setter(1,String::from($val)); + assert!(changed); + profiles.$getter(1, |x| { assert_eq!(x, $val); }); + //check that the other values are unaffected. + profiles.$getter(0, |x| { assert_eq!(oldvals[0], x); }); + profiles.$getter(2, |x| { assert_eq!(oldvals[1], x); }); + //IMPORTANT: check that assigning the same value returns not-changed. + let changed = profiles.$setter(1,String::from($val)); + assert!(!changed); + })* + }; + } + string_setter_tests!( + set_characters_test,set_characters,characters,"ABCDEFG", + set_modifier_test, set_modifier, modifier, "ab", + set_prefix_test, set_prefix, prefix, "xy", + set_suffix_test, set_suffix, suffix, "zw", + set_username_test, set_username, username, "Zargothrax" + ); + simple_setter_tests!( + set_hash_algorithm_test, set_hash_algorithm, hash_algorithm, 5, + set_password_length_test, set_password_length, password_length, 12, + set_use_domain_test, set_use_domain, use_domain, false, + set_use_leet_test, set_use_leet, use_leet, 1, + set_use_port_path_test, set_use_port_path, use_port_path, true, + set_use_protocol_test, set_use_protocol, use_protocol, true, + set_use_subdomains_test, set_use_subdomains, use_subdomains, true, + set_use_user_info_test, set_use_user_info, use_user_info, true, + set_use_undefined_as_protocol_fallback_test, set_use_undefined_as_protocol_fallback, use_undefined_as_protocol_fallback, false + ); +} diff --git a/rust/src/implementation/pwm_macros.rs b/rust/src/implementation/pwm_macros.rs new file mode 100644 index 0000000..158e4cd --- /dev/null +++ b/rust/src/implementation/pwm_macros.rs @@ -0,0 +1,27 @@ +extern crate passwordmaker_macros; +pub use self::passwordmaker_macros::*; + +pub trait EnumVariantCount { + fn variant_count() -> usize; +} + +#[cfg(test)] +mod pwm_macro_tests { + use super::*; + + #[allow(dead_code)] + enum Nest{ A, B } + #[allow(dead_code)] + #[derive(EnumVariantCount)] + enum TestNum { + A(usize), + B, + C(Nest), + D, + E{a : usize, b: f64} + } + #[test] + fn enum_variant_count_test(){ + assert_eq!(TestNum::variant_count(), 5); + } +}
\ No newline at end of file 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<T> { + data: T, + some: bool, +} + +impl<T> COption<T> { + #![allow(dead_code)] + fn into(self) -> Option<T> { + if self.some { + Some(self.data) + } else { + None + } + } +} + +impl<T> From<Option<T>> for COption<T> +where + T: Default, +{ + fn from(t: Option<T>) -> COption<T> { + if let Some(v) = t { + COption { + data: v, + some: true, + } + } else { + COption { + data: T::default(), + some: false, + } + } + } +} + + +pub enum QString {} + +fn set_string_from_utf16(s: &mut String, str: *const c_ushort, len: c_int) { + let utf16 = unsafe { slice::from_raw_parts(str, to_usize(len)) }; + let characters = decode_utf16(utf16.iter().cloned()) + .map(|r| r.unwrap()); + s.clear(); + s.extend(characters); +} + + + +#[repr(C)] +#[derive(PartialEq, Eq, Debug)] +pub enum SortOrder { + Ascending = 0, + Descending = 1, +} + +#[repr(C)] +pub struct QModelIndex { + row: c_int, + internal_id: usize, +} + + +fn to_usize(n: c_int) -> usize { + if n < 0 { + panic!("Cannot cast {} to usize", n); + } + n as usize +} + + +fn to_c_int(n: usize) -> c_int { + if n > c_int::max_value() as usize { + panic!("Cannot cast {} to c_int", n); + } + n as c_int +} + + +pub struct PasswordMakerQObject {} + +pub struct PasswordMakerEmitter { + qobject: Arc<AtomicPtr<PasswordMakerQObject>>, + generated_password_changed: extern fn(*mut PasswordMakerQObject), + generator_state_changed: extern fn(*mut PasswordMakerQObject), + i_say_sexy_things_to_myself_while_im_dancing_changed: extern fn(*mut PasswordMakerQObject), + master_password_changed: extern fn(*mut PasswordMakerQObject), + url_changed: extern fn(*mut PasswordMakerQObject), + used_text_changed: extern fn(*mut PasswordMakerQObject), +} + +unsafe impl Send for PasswordMakerEmitter {} + +#[cfg_attr(test, automock)] +#[cfg_attr(test,allow(dead_code))] +impl PasswordMakerEmitter { + /// Clone the emitter + pub fn clone(&self) -> Self { + Self { + qobject: self.qobject.clone(), + generated_password_changed: self.generated_password_changed, + generator_state_changed: self.generator_state_changed, + i_say_sexy_things_to_myself_while_im_dancing_changed: self.i_say_sexy_things_to_myself_while_im_dancing_changed, + master_password_changed: self.master_password_changed, + url_changed: self.url_changed, + used_text_changed: self.used_text_changed, + } + } + fn clear(&self) { + let n: *const PasswordMakerQObject = null(); + self.qobject.store(n as *mut PasswordMakerQObject, Ordering::SeqCst); + } + pub fn generated_password_changed(&self) { + let ptr = self.qobject.load(Ordering::SeqCst); + if !ptr.is_null() { + (self.generated_password_changed)(ptr); + } + } + pub fn generator_state_changed(&self) { + let ptr = self.qobject.load(Ordering::SeqCst); + if !ptr.is_null() { + (self.generator_state_changed)(ptr); + } + } + pub fn i_say_sexy_things_to_myself_while_im_dancing_changed(&self) { + let ptr = self.qobject.load(Ordering::SeqCst); + if !ptr.is_null() { + (self.i_say_sexy_things_to_myself_while_im_dancing_changed)(ptr); + } + } + pub fn master_password_changed(&self) { + let ptr = self.qobject.load(Ordering::SeqCst); + if !ptr.is_null() { + (self.master_password_changed)(ptr); + } + } + pub fn url_changed(&self) { + let ptr = self.qobject.load(Ordering::SeqCst); + if !ptr.is_null() { + (self.url_changed)(ptr); + } + } + pub fn used_text_changed(&self) { + let ptr = self.qobject.load(Ordering::SeqCst); + if !ptr.is_null() { + (self.used_text_changed)(ptr); + } + } +} + +pub trait PasswordMakerTrait { + #[cfg(not(test))] + fn new(emit: PasswordMakerEmitter, + profiles: Profiles, + settings: Settings) -> Self; + #[cfg(not(test))] + fn emit(&self) -> &PasswordMakerEmitter; + #[cfg(test)] + fn new(emit: MockPasswordMakerEmitter, + profiles: Profiles, + settings: Settings) -> Self; + #[cfg(test)] + fn emit(&self) -> &MockPasswordMakerEmitter; + fn generated_password<F>(&self, setter: F) where F: FnOnce(&str); + fn generator_state(&self) -> u8; + fn i_say_sexy_things_to_myself_while_im_dancing(&self) -> bool; + fn set_i_say_sexy_things_to_myself_while_im_dancing(&self, value: bool); + fn master_password<F>(&self, setter: F) where F: FnOnce(&str); + fn set_master_password(&self, value: String); + fn profiles(&self) -> &Profiles; + fn settings(&self) -> &Settings; + fn url<F>(&self, setter: F) where F: FnOnce(&str); + fn set_url(&self, value: String); + fn used_text<F>(&self, setter: F) where F: FnOnce(&str); + fn set_used_text(&self, value: String); + fn profile_changed(&self) -> (); + fn store_settings(&self) -> bool; +} +#[cfg(not(test))] +#[no_mangle] +pub extern "C" fn password_maker_new( + password_maker: *mut PasswordMakerQObject, + password_maker_generated_password_changed: extern fn(*mut PasswordMakerQObject), + password_maker_generator_state_changed: extern fn(*mut PasswordMakerQObject), + password_maker_i_say_sexy_things_to_myself_while_im_dancing_changed: extern fn(*mut PasswordMakerQObject), + password_maker_master_password_changed: extern fn(*mut PasswordMakerQObject), + profiles: *mut ProfilesQObject, + profiles_current_profile_changed: extern fn(*mut ProfilesQObject), + profiles_current_profile_name_changed: extern fn(*mut ProfilesQObject), + profiles_new_data_ready: extern fn(*mut ProfilesQObject), + profiles_layout_about_to_be_changed: extern fn(*mut ProfilesQObject), + profiles_layout_changed: extern fn(*mut ProfilesQObject), + profiles_data_changed: extern fn(*mut ProfilesQObject, usize, usize), + profiles_begin_reset_model: extern fn(*mut ProfilesQObject), + profiles_end_reset_model: extern fn(*mut ProfilesQObject), + profiles_begin_insert_rows: extern fn(*mut ProfilesQObject, usize, usize), + profiles_end_insert_rows: extern fn(*mut ProfilesQObject), + profiles_begin_move_rows: extern fn(*mut ProfilesQObject, usize, usize, usize), + profiles_end_move_rows: extern fn(*mut ProfilesQObject), + profiles_begin_remove_rows: extern fn(*mut ProfilesQObject, usize, usize), + profiles_end_remove_rows: extern fn(*mut ProfilesQObject), + settings: *mut SettingsQObject, + settings_clear_generated_password_seconds_changed: extern fn(*mut SettingsQObject), + settings_clear_master_password_seconds_changed: extern fn(*mut SettingsQObject), + settings_hide_generated_password_changed: extern fn(*mut SettingsQObject), + password_maker_url_changed: extern fn(*mut PasswordMakerQObject), + password_maker_used_text_changed: extern fn(*mut PasswordMakerQObject), +) -> *mut PasswordMaker { + let profiles_emit = ProfilesEmitter { + qobject: Arc::new(AtomicPtr::new(profiles)), + current_profile_changed: profiles_current_profile_changed, + current_profile_name_changed: profiles_current_profile_name_changed, + new_data_ready: profiles_new_data_ready, + }; + let model = ProfilesList { + qobject: profiles, + layout_about_to_be_changed: profiles_layout_about_to_be_changed, + layout_changed: profiles_layout_changed, + data_changed: profiles_data_changed, + begin_reset_model: profiles_begin_reset_model, + end_reset_model: profiles_end_reset_model, + begin_insert_rows: profiles_begin_insert_rows, + end_insert_rows: profiles_end_insert_rows, + begin_move_rows: profiles_begin_move_rows, + end_move_rows: profiles_end_move_rows, + begin_remove_rows: profiles_begin_remove_rows, + end_remove_rows: profiles_end_remove_rows, + }; + let d_profiles = Profiles::new(profiles_emit, model); + let settings_emit = SettingsEmitter { + qobject: Arc::new(AtomicPtr::new(settings)), + clear_generated_password_seconds_changed: settings_clear_generated_password_seconds_changed, + clear_master_password_seconds_changed: settings_clear_master_password_seconds_changed, + hide_generated_password_changed: settings_hide_generated_password_changed, + }; + let d_settings = Settings::new(settings_emit); + let password_maker_emit = PasswordMakerEmitter { + qobject: Arc::new(AtomicPtr::new(password_maker)), + generated_password_changed: password_maker_generated_password_changed, + generator_state_changed: password_maker_generator_state_changed, + i_say_sexy_things_to_myself_while_im_dancing_changed: password_maker_i_say_sexy_things_to_myself_while_im_dancing_changed, + master_password_changed: password_maker_master_password_changed, + url_changed: password_maker_url_changed, + used_text_changed: password_maker_used_text_changed, + }; + let d_password_maker = PasswordMaker::new(password_maker_emit, + d_profiles, + d_settings); + Box::into_raw(Box::new(d_password_maker)) +} + +#[no_mangle] +pub unsafe extern "C" fn password_maker_free(ptr: *mut PasswordMaker) { + Box::from_raw(ptr).emit().clear(); +} + +#[no_mangle] +pub unsafe extern "C" fn password_maker_generated_password_get( +ptr: *const PasswordMaker, +p: *mut QString, +set: extern fn(*mut QString, *const c_char, c_int), +) { +let o = &*ptr; +o.generated_password(|v| { + let s: *const c_char = v.as_ptr() as *const c_char; + set(p, s, to_c_int(v.len())); +}); +} + +#[no_mangle] +pub unsafe extern "C" fn password_maker_generator_state_get(ptr: *const PasswordMaker) -> u8 { + (&*ptr).generator_state() +} + +#[no_mangle] +pub unsafe extern "C" fn password_maker_i_say_sexy_things_to_myself_while_im_dancing_get(ptr: *const PasswordMaker) -> bool { + (&*ptr).i_say_sexy_things_to_myself_while_im_dancing() +} + +#[no_mangle] +pub unsafe extern "C" fn password_maker_i_say_sexy_things_to_myself_while_im_dancing_set(ptr: *const PasswordMaker, v: bool) { + (&*ptr).set_i_say_sexy_things_to_myself_while_im_dancing(v); +} + +#[no_mangle] +pub unsafe extern "C" fn password_maker_master_password_get( +ptr: *const PasswordMaker, +p: *mut QString, +set: extern fn(*mut QString, *const c_char, c_int), +) { +let o = &*ptr; +o.master_password(|v| { + let s: *const c_char = v.as_ptr() as *const c_char; + set(p, s, to_c_int(v.len())); +}); +} + +#[no_mangle] +pub unsafe extern "C" fn password_maker_master_password_set(ptr: *const PasswordMaker, v: *const c_ushort, len: c_int) { + let o = &*ptr; + let mut s = String::new(); + set_string_from_utf16(&mut s, v, len); + o.set_master_password(s); +} + +#[no_mangle] +pub unsafe extern "C" fn password_maker_profiles_get(ptr: *const PasswordMaker) -> *const Profiles { + (&*ptr).profiles() +} + +#[no_mangle] +pub unsafe extern "C" fn password_maker_settings_get(ptr: *const PasswordMaker) -> *const Settings { + (&*ptr).settings() +} + +#[no_mangle] +pub unsafe extern "C" fn password_maker_url_get( +ptr: *const PasswordMaker, +p: *mut QString, +set: extern fn(*mut QString, *const c_char, c_int), +) { +let o = &*ptr; +o.url(|v| { + let s: *const c_char = v.as_ptr() as *const c_char; + set(p, s, to_c_int(v.len())); +}); +} + +#[no_mangle] +pub unsafe extern "C" fn password_maker_url_set(ptr: *const PasswordMaker, v: *const c_ushort, len: c_int) { + let o = &*ptr; + let mut s = String::new(); + set_string_from_utf16(&mut s, v, len); + o.set_url(s); +} + +#[no_mangle] +pub unsafe extern "C" fn password_maker_used_text_get( +ptr: *const PasswordMaker, +p: *mut QString, +set: extern fn(*mut QString, *const c_char, c_int), +) { +let o = &*ptr; +o.used_text(|v| { + let s: *const c_char = v.as_ptr() as *const c_char; + set(p, s, to_c_int(v.len())); +}); +} + +#[no_mangle] +pub unsafe extern "C" fn password_maker_used_text_set(ptr: *const PasswordMaker, v: *const c_ushort, len: c_int) { + let o = &*ptr; + let mut s = String::new(); + set_string_from_utf16(&mut s, v, len); + o.set_used_text(s); +} + +#[no_mangle] +pub unsafe extern "C" fn password_maker_profile_changed(ptr: *const PasswordMaker) { + let o = &*ptr; + o.profile_changed() +} + +#[no_mangle] +pub unsafe extern "C" fn password_maker_store_settings(ptr: *const PasswordMaker) -> bool { + let o = &*ptr; + o.store_settings() +} + +pub struct ProfilesQObject {} + +pub struct ProfilesEmitter { + qobject: Arc<AtomicPtr<ProfilesQObject>>, + current_profile_changed: extern fn(*mut ProfilesQObject), + current_profile_name_changed: extern fn(*mut ProfilesQObject), + new_data_ready: extern fn(*mut ProfilesQObject), +} + +unsafe impl Send for ProfilesEmitter {} + +#[cfg_attr(test, automock)] +#[cfg_attr(test,allow(dead_code))] +impl ProfilesEmitter { + /// Clone the emitter + pub fn clone(&self) -> Self { + Self { + qobject: self.qobject.clone(), + current_profile_changed: self.current_profile_changed, + current_profile_name_changed: self.current_profile_name_changed, + new_data_ready: self.new_data_ready, + } + } + fn clear(&self) { + let n: *const ProfilesQObject = null(); + self.qobject.store(n as *mut ProfilesQObject, Ordering::SeqCst); + } + pub fn current_profile_changed(&self) { + let ptr = self.qobject.load(Ordering::SeqCst); + if !ptr.is_null() { + (self.current_profile_changed)(ptr); + } + } + pub fn current_profile_name_changed(&self) { + let ptr = self.qobject.load(Ordering::SeqCst); + if !ptr.is_null() { + (self.current_profile_name_changed)(ptr); + } + } + pub fn new_data_ready(&self) { + let ptr = self.qobject.load(Ordering::SeqCst); + if !ptr.is_null() { + (self.new_data_ready)(ptr); + } + } +} + +#[derive(Clone)] +pub struct ProfilesList { + qobject: *mut ProfilesQObject, + layout_about_to_be_changed: extern fn(*mut ProfilesQObject), + layout_changed: extern fn(*mut ProfilesQObject), + data_changed: extern fn(*mut ProfilesQObject, usize, usize), + begin_reset_model: extern fn(*mut ProfilesQObject), + end_reset_model: extern fn(*mut ProfilesQObject), + begin_insert_rows: extern fn(*mut ProfilesQObject, usize, usize), + end_insert_rows: extern fn(*mut ProfilesQObject), + begin_move_rows: extern fn(*mut ProfilesQObject, usize, usize, usize), + end_move_rows: extern fn(*mut ProfilesQObject), + begin_remove_rows: extern fn(*mut ProfilesQObject, usize, usize), + end_remove_rows: extern fn(*mut ProfilesQObject), +} + + +#[cfg_attr(test, automock)] +impl ProfilesList { + pub fn layout_about_to_be_changed(&self) { + (self.layout_about_to_be_changed)(self.qobject); + } + pub fn layout_changed(&self) { + (self.layout_changed)(self.qobject); + } + pub fn data_changed(&self, first: usize, last: usize) { + (self.data_changed)(self.qobject, first, last); + } + pub fn begin_reset_model(&self) { + (self.begin_reset_model)(self.qobject); + } + pub fn end_reset_model(&self) { + (self.end_reset_model)(self.qobject); + } + pub fn begin_insert_rows(&self, first: usize, last: usize) { + (self.begin_insert_rows)(self.qobject, first, last); + } + pub fn end_insert_rows(&self) { + (self.end_insert_rows)(self.qobject); + } + pub fn begin_move_rows(&self, first: usize, last: usize, destination: usize) { + (self.begin_move_rows)(self.qobject, first, last, destination); + } + pub fn end_move_rows(&self) { + (self.end_move_rows)(self.qobject); + } + pub fn begin_remove_rows(&self, first: usize, last: usize) { + (self.begin_remove_rows)(self.qobject, first, last); + } + pub fn end_remove_rows(&self) { + (self.end_remove_rows)(self.qobject); + } +} + +pub trait ProfilesTrait { + #[cfg(not(test))] + fn new(emit: ProfilesEmitter, model: ProfilesList) -> Self; + #[cfg(not(test))] + fn emit(&self) -> &ProfilesEmitter; + #[cfg(test)] + fn new(emit: MockProfilesEmitter, model: MockProfilesList) -> Self; + #[cfg(test)] + fn emit(&self) -> &MockProfilesEmitter; + fn current_profile(&self) -> u32; + fn set_current_profile(&self, value: u32); + fn current_profile_name<F>(&self, setter: F) where F: FnOnce(&str); + fn store(&self) -> bool; + fn row_count(&self) -> usize; + fn insert_rows(&self, _row: usize, _count: usize) -> bool { false } + fn remove_rows(&self, _row: usize, _count: usize) -> bool { false } + fn can_fetch_more(&self) -> bool { + false + } + fn fetch_more(&self) {} + fn sort(&self, _: u8, _: SortOrder) {} + fn characters<F: FnOnce(&str)>(&self, index: usize, setter: F); + fn set_characters(&self, index: usize, _: String) -> bool; + fn hash_algorithm(&self, index: usize) -> u8; + fn set_hash_algorithm(&self, index: usize, _: u8) -> bool; + fn leet_level(&self, index: usize) -> u8; + fn set_leet_level(&self, index: usize, _: u8) -> bool; + fn modifier<F: FnOnce(&str)>(&self, index: usize, setter: F); + fn set_modifier(&self, index: usize, _: String) -> bool; + fn name<F: FnOnce(&str)>(&self, index: usize, setter: F); + fn set_name(&self, index: usize, _: String) -> bool; + fn password_length(&self, index: usize) -> u32; + fn set_password_length(&self, index: usize, _: u32) -> bool; + fn prefix<F: FnOnce(&str)>(&self, index: usize, setter: F); + fn set_prefix(&self, index: usize, _: String) -> bool; + fn suffix<F: FnOnce(&str)>(&self, index: usize, setter: F); + fn set_suffix(&self, index: usize, _: String) -> bool; + fn use_domain(&self, index: usize) -> bool; + fn set_use_domain(&self, index: usize, _: bool) -> bool; + fn use_leet(&self, index: usize) -> u8; + fn set_use_leet(&self, index: usize, _: u8) -> bool; + fn use_port_path(&self, index: usize) -> bool; + fn set_use_port_path(&self, index: usize, _: bool) -> bool; + fn use_protocol(&self, index: usize) -> bool; + fn set_use_protocol(&self, index: usize, _: bool) -> bool; + fn use_subdomains(&self, index: usize) -> bool; + fn set_use_subdomains(&self, index: usize, _: bool) -> bool; + fn use_undefined_as_protocol_fallback(&self, index: usize) -> bool; + fn set_use_undefined_as_protocol_fallback(&self, index: usize, _: bool) -> bool; + fn use_user_info(&self, index: usize) -> bool; + fn set_use_user_info(&self, index: usize, _: bool) -> bool; + fn username<F: FnOnce(&str)>(&self, index: usize, setter: F); + fn set_username(&self, index: usize, _: String) -> bool; +} +#[cfg(not(test))] +#[no_mangle] +pub extern "C" fn profiles_new( + profiles: *mut ProfilesQObject, + profiles_current_profile_changed: extern fn(*mut ProfilesQObject), + profiles_current_profile_name_changed: extern fn(*mut ProfilesQObject), + profiles_new_data_ready: extern fn(*mut ProfilesQObject), + profiles_layout_about_to_be_changed: extern fn(*mut ProfilesQObject), + profiles_layout_changed: extern fn(*mut ProfilesQObject), + profiles_data_changed: extern fn(*mut ProfilesQObject, usize, usize), + profiles_begin_reset_model: extern fn(*mut ProfilesQObject), + profiles_end_reset_model: extern fn(*mut ProfilesQObject), + profiles_begin_insert_rows: extern fn(*mut ProfilesQObject, usize, usize), + profiles_end_insert_rows: extern fn(*mut ProfilesQObject), + profiles_begin_move_rows: extern fn(*mut ProfilesQObject, usize, usize, usize), + profiles_end_move_rows: extern fn(*mut ProfilesQObject), + profiles_begin_remove_rows: extern fn(*mut ProfilesQObject, usize, usize), + profiles_end_remove_rows: extern fn(*mut ProfilesQObject), +) -> *mut Profiles { + let profiles_emit = ProfilesEmitter { + qobject: Arc::new(AtomicPtr::new(profiles)), + current_profile_changed: profiles_current_profile_changed, + current_profile_name_changed: profiles_current_profile_name_changed, + new_data_ready: profiles_new_data_ready, + }; + let model = ProfilesList { + qobject: profiles, + layout_about_to_be_changed: profiles_layout_about_to_be_changed, + layout_changed: profiles_layout_changed, + data_changed: profiles_data_changed, + begin_reset_model: profiles_begin_reset_model, + end_reset_model: profiles_end_reset_model, + begin_insert_rows: profiles_begin_insert_rows, + end_insert_rows: profiles_end_insert_rows, + begin_move_rows: profiles_begin_move_rows, + end_move_rows: profiles_end_move_rows, + begin_remove_rows: profiles_begin_remove_rows, + end_remove_rows: profiles_end_remove_rows, + }; + let d_profiles = Profiles::new(profiles_emit, model); + Box::into_raw(Box::new(d_profiles)) +} + +#[no_mangle] +pub unsafe extern "C" fn profiles_free(ptr: *mut Profiles) { + Box::from_raw(ptr).emit().clear(); +} + +#[no_mangle] +pub unsafe extern "C" fn profiles_current_profile_get(ptr: *const Profiles) -> u32 { + (&*ptr).current_profile() +} + +#[no_mangle] +pub unsafe extern "C" fn profiles_current_profile_set(ptr: *const Profiles, v: u32) { + (&*ptr).set_current_profile(v); +} + +#[no_mangle] +pub unsafe extern "C" fn profiles_current_profile_name_get( +ptr: *const Profiles, +p: *mut QString, +set: extern fn(*mut QString, *const c_char, c_int), +) { +let o = &*ptr; +o.current_profile_name(|v| { + let s: *const c_char = v.as_ptr() as *const c_char; + set(p, s, to_c_int(v.len())); +}); +} + +#[no_mangle] +pub unsafe extern "C" fn profiles_store(ptr: *const Profiles) -> bool { + let o = &*ptr; + o.store() +} + +#[no_mangle] +pub unsafe extern "C" fn profiles_row_count(ptr: *const Profiles) -> c_int { + to_c_int((&*ptr).row_count()) +} +#[no_mangle] +pub unsafe extern "C" fn profiles_insert_rows(ptr: *const Profiles, row: c_int, count: c_int) -> bool { + (&*ptr).insert_rows(to_usize(row), to_usize(count)) +} +#[no_mangle] +pub unsafe extern "C" fn profiles_remove_rows(ptr: *const Profiles, row: c_int, count: c_int) -> bool { + (&*ptr).remove_rows(to_usize(row), to_usize(count)) +} +#[no_mangle] +pub unsafe extern "C" fn profiles_can_fetch_more(ptr: *const Profiles) -> bool { + (&*ptr).can_fetch_more() +} +#[no_mangle] +pub unsafe extern "C" fn profiles_fetch_more(ptr: *const Profiles) { + (&*ptr).fetch_more() +} +#[no_mangle] +pub unsafe extern "C" fn profiles_sort( + ptr: *const Profiles, + column: u8, + order: SortOrder, +) { + (&*ptr).sort(column, order) +} + +#[no_mangle] +pub unsafe extern "C" fn profiles_data_characters( + ptr: *const Profiles, row: c_int, + d: *mut QString, + set: extern fn(*mut QString, *const c_char, len: c_int), +) { + let o = &*ptr; + o.characters(to_usize(row), |data| { + let s: *const c_char = data.as_ptr() as *const c_char; + set(d, s, to_c_int(data.len())); + }) + } + +#[no_mangle] +pub unsafe extern "C" fn profiles_set_data_characters( + ptr: *const Profiles, row: c_int, + s: *const c_ushort, len: c_int, +) -> bool { + let o = &*ptr; + let mut v = String::new(); + set_string_from_utf16(&mut v, s, len); + o.set_characters(to_usize(row), v) +} + +#[no_mangle] +pub unsafe extern "C" fn profiles_data_hash_algorithm(ptr: *const Profiles, row: c_int) -> u8 { + let o = &*ptr; + o.hash_algorithm(to_usize(row)) +} + +#[no_mangle] +pub unsafe extern "C" fn profiles_set_data_hash_algorithm( + ptr: *const Profiles, row: c_int, + v: u8, +) -> bool { + (&*ptr).set_hash_algorithm(to_usize(row), v) +} + +#[no_mangle] +pub unsafe extern "C" fn profiles_data_leet_level(ptr: *const Profiles, row: c_int) -> u8 { + let o = &*ptr; + o.leet_level(to_usize(row)) +} + +#[no_mangle] +pub unsafe extern "C" fn profiles_set_data_leet_level( + ptr: *const Profiles, row: c_int, + v: u8, +) -> bool { + (&*ptr).set_leet_level(to_usize(row), v) +} + +#[no_mangle] +pub unsafe extern "C" fn profiles_data_modifier( + ptr: *const Profiles, row: c_int, + d: *mut QString, + set: extern fn(*mut QString, *const c_char, len: c_int), +) { + let o = &*ptr; + o.modifier(to_usize(row), |data| { + let s: *const c_char = data.as_ptr() as *const c_char; + set(d, s, to_c_int(data.len())); + }) + } + +#[no_mangle] +pub unsafe extern "C" fn profiles_set_data_modifier( + ptr: *const Profiles, row: c_int, + s: *const c_ushort, len: c_int, +) -> bool { + let o = &*ptr; + let mut v = String::new(); + set_string_from_utf16(&mut v, s, len); + o.set_modifier(to_usize(row), v) +} + +#[no_mangle] +pub unsafe extern "C" fn profiles_data_name( + ptr: *const Profiles, row: c_int, + d: *mut QString, + set: extern fn(*mut QString, *const c_char, len: c_int), +) { + let o = &*ptr; + o.name(to_usize(row), |data| { + let s: *const c_char = data.as_ptr() as *const c_char; + set(d, s, to_c_int(data.len())); + }) + } + +#[no_mangle] +pub unsafe extern "C" fn profiles_set_data_name( + ptr: *const Profiles, row: c_int, + s: *const c_ushort, len: c_int, +) -> bool { + let o = &*ptr; + let mut v = String::new(); + set_string_from_utf16(&mut v, s, len); + o.set_name(to_usize(row), v) +} + +#[no_mangle] +pub unsafe extern "C" fn profiles_data_password_length(ptr: *const Profiles, row: c_int) -> u32 { + let o = &*ptr; + o.password_length(to_usize(row)) +} + +#[no_mangle] +pub unsafe extern "C" fn profiles_set_data_password_length( + ptr: *const Profiles, row: c_int, + v: u32, +) -> bool { + (&*ptr).set_password_length(to_usize(row), v) +} + +#[no_mangle] +pub unsafe extern "C" fn profiles_data_prefix( + ptr: *const Profiles, row: c_int, + d: *mut QString, + set: extern fn(*mut QString, *const c_char, len: c_int), +) { + let o = &*ptr; + o.prefix(to_usize(row), |data| { + let s: *const c_char = data.as_ptr() as *const c_char; + set(d, s, to_c_int(data.len())); + }) + } + +#[no_mangle] +pub unsafe extern "C" fn profiles_set_data_prefix( + ptr: *const Profiles, row: c_int, + s: *const c_ushort, len: c_int, +) -> bool { + let o = &*ptr; + let mut v = String::new(); + set_string_from_utf16(&mut v, s, len); + o.set_prefix(to_usize(row), v) +} + +#[no_mangle] +pub unsafe extern "C" fn profiles_data_suffix( + ptr: *const Profiles, row: c_int, + d: *mut QString, + set: extern fn(*mut QString, *const c_char, len: c_int), +) { + let o = &*ptr; + o.suffix(to_usize(row), |data| { + let s: *const c_char = data.as_ptr() as *const c_char; + set(d, s, to_c_int(data.len())); + }) + } + +#[no_mangle] +pub unsafe extern "C" fn profiles_set_data_suffix( + ptr: *const Profiles, row: c_int, + s: *const c_ushort, len: c_int, +) -> bool { + let o = &*ptr; + let mut v = String::new(); + set_string_from_utf16(&mut v, s, len); + o.set_suffix(to_usize(row), v) +} + +#[no_mangle] +pub unsafe extern "C" fn profiles_data_use_domain(ptr: *const Profiles, row: c_int) -> bool { + let o = &*ptr; + o.use_domain(to_usize(row)) +} + +#[no_mangle] +pub unsafe extern "C" fn profiles_set_data_use_domain( + ptr: *const Profiles, row: c_int, + v: bool, +) -> bool { + (&*ptr).set_use_domain(to_usize(row), v) +} + +#[no_mangle] +pub unsafe extern "C" fn profiles_data_use_leet(ptr: *const Profiles, row: c_int) -> u8 { + let o = &*ptr; + o.use_leet(to_usize(row)) +} + +#[no_mangle] +pub unsafe extern "C" fn profiles_set_data_use_leet( + ptr: *const Profiles, row: c_int, + v: u8, +) -> bool { + (&*ptr).set_use_leet(to_usize(row), v) +} + +#[no_mangle] +pub unsafe extern "C" fn profiles_data_use_port_path(ptr: *const Profiles, row: c_int) -> bool { + let o = &*ptr; + o.use_port_path(to_usize(row)) +} + +#[no_mangle] +pub unsafe extern "C" fn profiles_set_data_use_port_path( + ptr: *const Profiles, row: c_int, + v: bool, +) -> bool { + (&*ptr).set_use_port_path(to_usize(row), v) +} + +#[no_mangle] +pub unsafe extern "C" fn profiles_data_use_protocol(ptr: *const Profiles, row: c_int) -> bool { + let o = &*ptr; + o.use_protocol(to_usize(row)) +} + +#[no_mangle] +pub unsafe extern "C" fn profiles_set_data_use_protocol( + ptr: *const Profiles, row: c_int, + v: bool, +) -> bool { + (&*ptr).set_use_protocol(to_usize(row), v) +} + +#[no_mangle] +pub unsafe extern "C" fn profiles_data_use_subdomains(ptr: *const Profiles, row: c_int) -> bool { + let o = &*ptr; + o.use_subdomains(to_usize(row)) +} + +#[no_mangle] +pub unsafe extern "C" fn profiles_set_data_use_subdomains( + ptr: *const Profiles, row: c_int, + v: bool, +) -> bool { + (&*ptr).set_use_subdomains(to_usize(row), v) +} + +#[no_mangle] +pub unsafe extern "C" fn profiles_data_use_undefined_as_protocol_fallback(ptr: *const Profiles, row: c_int) -> bool { + let o = &*ptr; + o.use_undefined_as_protocol_fallback(to_usize(row)) +} + +#[no_mangle] +pub unsafe extern "C" fn profiles_set_data_use_undefined_as_protocol_fallback( + ptr: *const Profiles, row: c_int, + v: bool, +) -> bool { + (&*ptr).set_use_undefined_as_protocol_fallback(to_usize(row), v) +} + +#[no_mangle] +pub unsafe extern "C" fn profiles_data_use_user_info(ptr: *const Profiles, row: c_int) -> bool { + let o = &*ptr; + o.use_user_info(to_usize(row)) +} + +#[no_mangle] +pub unsafe extern "C" fn profiles_set_data_use_user_info( + ptr: *const Profiles, row: c_int, + v: bool, +) -> bool { + (&*ptr).set_use_user_info(to_usize(row), v) +} + +#[no_mangle] +pub unsafe extern "C" fn profiles_data_username( + ptr: *const Profiles, row: c_int, + d: *mut QString, + set: extern fn(*mut QString, *const c_char, len: c_int), +) { + let o = &*ptr; + o.username(to_usize(row), |data| { + let s: *const c_char = data.as_ptr() as *const c_char; + set(d, s, to_c_int(data.len())); + }) + } + +#[no_mangle] +pub unsafe extern "C" fn profiles_set_data_username( + ptr: *const Profiles, row: c_int, + s: *const c_ushort, len: c_int, +) -> bool { + let o = &*ptr; + let mut v = String::new(); + set_string_from_utf16(&mut v, s, len); + o.set_username(to_usize(row), v) +} + +pub struct SettingsQObject {} + +pub struct SettingsEmitter { + qobject: Arc<AtomicPtr<SettingsQObject>>, + clear_generated_password_seconds_changed: extern fn(*mut SettingsQObject), + clear_master_password_seconds_changed: extern fn(*mut SettingsQObject), + hide_generated_password_changed: extern fn(*mut SettingsQObject), +} + +unsafe impl Send for SettingsEmitter {} + +#[cfg_attr(test, automock)] +#[cfg_attr(test,allow(dead_code))] +impl SettingsEmitter { + /// Clone the emitter + pub fn clone(&self) -> Self { + Self { + qobject: self.qobject.clone(), + clear_generated_password_seconds_changed: self.clear_generated_password_seconds_changed, + clear_master_password_seconds_changed: self.clear_master_password_seconds_changed, + hide_generated_password_changed: self.hide_generated_password_changed, + } + } + fn clear(&self) { + let n: *const SettingsQObject = null(); + self.qobject.store(n as *mut SettingsQObject, Ordering::SeqCst); + } + pub fn clear_generated_password_seconds_changed(&self) { + let ptr = self.qobject.load(Ordering::SeqCst); + if !ptr.is_null() { + (self.clear_generated_password_seconds_changed)(ptr); + } + } + pub fn clear_master_password_seconds_changed(&self) { + let ptr = self.qobject.load(Ordering::SeqCst); + if !ptr.is_null() { + (self.clear_master_password_seconds_changed)(ptr); + } + } + pub fn hide_generated_password_changed(&self) { + let ptr = self.qobject.load(Ordering::SeqCst); + if !ptr.is_null() { + (self.hide_generated_password_changed)(ptr); + } + } +} + +pub trait SettingsTrait { + #[cfg(not(test))] + fn new(emit: SettingsEmitter) -> Self; + #[cfg(not(test))] + fn emit(&self) -> &SettingsEmitter; + #[cfg(test)] + fn new(emit: MockSettingsEmitter) -> Self; + #[cfg(test)] + fn emit(&self) -> &MockSettingsEmitter; + fn clear_generated_password_seconds(&self) -> Option<u32>; + fn set_clear_generated_password_seconds(&self, value: Option<u32>); + fn clear_master_password_seconds(&self) -> Option<u32>; + fn set_clear_master_password_seconds(&self, value: Option<u32>); + fn hide_generated_password(&self) -> bool; + fn set_hide_generated_password(&self, value: bool); +} +#[cfg(not(test))] +#[no_mangle] +pub extern "C" fn settings_new( + settings: *mut SettingsQObject, + settings_clear_generated_password_seconds_changed: extern fn(*mut SettingsQObject), + settings_clear_master_password_seconds_changed: extern fn(*mut SettingsQObject), + settings_hide_generated_password_changed: extern fn(*mut SettingsQObject), +) -> *mut Settings { + let settings_emit = SettingsEmitter { + qobject: Arc::new(AtomicPtr::new(settings)), + clear_generated_password_seconds_changed: settings_clear_generated_password_seconds_changed, + clear_master_password_seconds_changed: settings_clear_master_password_seconds_changed, + hide_generated_password_changed: settings_hide_generated_password_changed, + }; + let d_settings = Settings::new(settings_emit); + Box::into_raw(Box::new(d_settings)) +} + +#[no_mangle] +pub unsafe extern "C" fn settings_free(ptr: *mut Settings) { + Box::from_raw(ptr).emit().clear(); +} + +#[no_mangle] +pub unsafe extern "C" fn settings_clear_generated_password_seconds_get(ptr: *const Settings) -> COption<u32> { + match (&*ptr).clear_generated_password_seconds() { + Some(value) => COption { data: value, some: true }, + None => COption { data: u32::default(), some: false} + } +} + +#[no_mangle] +pub unsafe extern "C" fn settings_clear_generated_password_seconds_set(ptr: *const Settings, v: u32) { + (&*ptr).set_clear_generated_password_seconds(Some(v)); +} + +#[no_mangle] +pub unsafe extern "C" fn settings_clear_generated_password_seconds_set_none(ptr: *const Settings) { + let o = &*ptr; + o.set_clear_generated_password_seconds(None); +} + +#[no_mangle] +pub unsafe extern "C" fn settings_clear_master_password_seconds_get(ptr: *const Settings) -> COption<u32> { + match (&*ptr).clear_master_password_seconds() { + Some(value) => COption { data: value, some: true }, + None => COption { data: u32::default(), some: false} + } +} + +#[no_mangle] +pub unsafe extern "C" fn settings_clear_master_password_seconds_set(ptr: *const Settings, v: u32) { + (&*ptr).set_clear_master_password_seconds(Some(v)); +} + +#[no_mangle] +pub unsafe extern "C" fn settings_clear_master_password_seconds_set_none(ptr: *const Settings) { + let o = &*ptr; + o.set_clear_master_password_seconds(None); +} + +#[no_mangle] +pub unsafe extern "C" fn settings_hide_generated_password_get(ptr: *const Settings) -> bool { + (&*ptr).hide_generated_password() +} + +#[no_mangle] +pub unsafe extern "C" fn settings_hide_generated_password_set(ptr: *const Settings, v: bool) { + (&*ptr).set_hide_generated_password(v); +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs new file mode 100644 index 0000000..abdb917 --- /dev/null +++ b/rust/src/lib.rs @@ -0,0 +1,12 @@ +extern crate libc; +extern crate serde; +extern crate dirs; +extern crate mockall_double; + +#[cfg(test)] +extern crate mockall; + +#[allow(clippy::all)] +pub mod interface; +#[warn(clippy::pedantic)] +mod implementation; diff --git a/rust_macro/Cargo.lock b/rust_macro/Cargo.lock new file mode 100644 index 0000000..6c3146f --- /dev/null +++ b/rust_macro/Cargo.lock @@ -0,0 +1,46 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "passwordmaker_macros" +version = "1.0.0" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd" diff --git a/rust_macro/Cargo.toml b/rust_macro/Cargo.toml new file mode 100644 index 0000000..93291e2 --- /dev/null +++ b/rust_macro/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "passwordmaker_macros" +version = "1.0.0" + +[dependencies] +syn = "1.0" +quote = "1.0" + +[lib] +name = "passwordmaker_macros" +proc-macro = true
\ No newline at end of file diff --git a/rust_macro/src/lib.rs b/rust_macro/src/lib.rs new file mode 100644 index 0000000..1d4a5ae --- /dev/null +++ b/rust_macro/src/lib.rs @@ -0,0 +1,24 @@ +/// This is a dumping ground for all proc-macros used by passfish. +/// There's a corresponding runtime module in the main crate. +extern crate syn; +extern crate quote; +extern crate proc_macro; + +use proc_macro::TokenStream; +use quote::quote; + +/// Adds a global constant name ENUMTYPE_ +/// Thanks, StackOverflow: https://stackoverflow.com/questions/41637978/how-to-get-the-number-of-elements-variants-in-an-enum-as-a-constant-value +#[proc_macro_derive(EnumVariantCount)] +pub fn derive_enum_variant_count(input: TokenStream) -> TokenStream { + let syn_item: syn::DeriveInput = syn::parse(input).unwrap(); + let len = match syn_item.data { + syn::Data::Enum(enum_item) => enum_item.variants.len(), + _ => panic!("EnumVariantCount only works on Enums"), + }; + let enum_name = syn_item.ident; + let result = quote! { + impl EnumVariantCount for #enum_name { fn variant_count() -> usize { #len } } + }; + result.into() +} diff --git a/rust_testhelper/Cargo.lock b/rust_testhelper/Cargo.lock new file mode 100644 index 0000000..2e5f5d0 --- /dev/null +++ b/rust_testhelper/Cargo.lock @@ -0,0 +1,23 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "cc" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" + +[[package]] +name = "libc" +version = "0.2.133" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f80d65747a3e43d1596c7c5492d95d5edddaabd45a7fcdb02b95f644164966" + +[[package]] +name = "rust_testhelper" +version = "0.1.0" +dependencies = [ + "cc", + "libc", +] diff --git a/rust_testhelper/Cargo.toml b/rust_testhelper/Cargo.toml new file mode 100644 index 0000000..53958db --- /dev/null +++ b/rust_testhelper/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "rust_testhelper" +version = "0.1.0" +description = "Helper for compiling and linking pwm_qhash.cpp during cargo test." + +[dependencies] +libc = "0.2" + +[build-dependencies] +cc = "1.0" diff --git a/rust_testhelper/build.rs b/rust_testhelper/build.rs new file mode 100644 index 0000000..6fa9026 --- /dev/null +++ b/rust_testhelper/build.rs @@ -0,0 +1,9 @@ +fn main() { + cc::Build::new() + .cpp(true) + .file("../src/pwm_qhash.cpp") + .include("/usr/include/qt5/") + .include("/usr/include/qt5/QtCore/") + .compile("pwm_qhash"); + println!("cargo:rustc-link-lib=Qt5Core"); +} diff --git a/rust_testhelper/src/lib.rs b/rust_testhelper/src/lib.rs new file mode 100644 index 0000000..a3c5db5 --- /dev/null +++ b/rust_testhelper/src/lib.rs @@ -0,0 +1,6 @@ +extern crate libc; +use libc::size_t; + +extern "C"{ + pub fn pwm_qhash(algorithm : size_t, input : *const u8, input_length : size_t, output : *mut u8, output_capacity : size_t) -> size_t; +} diff --git a/src/Bindings.cpp b/src/Bindings.cpp new file mode 100644 index 0000000..a673895 --- /dev/null +++ b/src/Bindings.cpp @@ -0,0 +1,1288 @@ +/* generated by rust_qt_binding_generator */ +/* Beware: a lot of const pointers mentioned in this file are mutating data on the Rust side. */ +#include "Bindings.h" + +namespace { + + struct option_quint32 { + public: + quint32 value; + bool some; + operator QVariant() const { + if (some) { + return QVariant::fromValue(value); + } + return QVariant(); + } + }; + static_assert(std::is_pod<option_quint32>::value, "option_quint32 must be a POD type."); + + struct option_quintptr { + public: + quintptr value; + bool some; + operator QVariant() const { + if (some) { + return QVariant::fromValue(value); + } + return QVariant(); + } + }; + static_assert(std::is_pod<option_quintptr>::value, "option_quintptr must be a POD type."); + + typedef void (*qstring_set)(QString* val, const char* utf8, int nbytes); + void set_qstring(QString* val, const char* utf8, int nbytes) { + *val = QString::fromUtf8(utf8, nbytes); + } + + struct qmodelindex_t { + int row; + quintptr id; + }; + inline QVariant cleanNullQVariant(const QVariant& v) { + return (v.isNull()) ?QVariant() :v; + } + inline void passwordMakerGenerated_passwordChanged(PasswordMaker* o) + { + Q_EMIT o->generated_passwordChanged(); + } + inline void passwordMakerGenerator_stateChanged(PasswordMaker* o) + { + Q_EMIT o->generator_stateChanged(); + } + inline void passwordMakerI_say_sexy_things_to_myself_while_im_dancingChanged(PasswordMaker* o) + { + Q_EMIT o->i_say_sexy_things_to_myself_while_im_dancingChanged(); + } + inline void passwordMakerMaster_passwordChanged(PasswordMaker* o) + { + Q_EMIT o->master_passwordChanged(); + } + inline void passwordMakerUrlChanged(PasswordMaker* o) + { + Q_EMIT o->urlChanged(); + } + inline void passwordMakerUsed_textChanged(PasswordMaker* o) + { + Q_EMIT o->used_textChanged(); + } + inline void profilesCurrent_profileChanged(Profiles* o) + { + Q_EMIT o->current_profileChanged(); + } + inline void profilesCurrent_profile_nameChanged(Profiles* o) + { + Q_EMIT o->current_profile_nameChanged(); + } + inline void settingsClear_generated_password_secondsChanged(Settings* o) + { + Q_EMIT o->clear_generated_password_secondsChanged(); + } + inline void settingsClear_master_password_secondsChanged(Settings* o) + { + Q_EMIT o->clear_master_password_secondsChanged(); + } + inline void settingsHide_generated_passwordChanged(Settings* o) + { + Q_EMIT o->hide_generated_passwordChanged(); + } +} +extern "C" { + PasswordMaker::Private* password_maker_new(PasswordMaker*, void (*)(PasswordMaker*), void (*)(PasswordMaker*), void (*)(PasswordMaker*), void (*)(PasswordMaker*), Profiles*, void (*)(Profiles*), void (*)(Profiles*), + void (*)(const Profiles*), + void (*)(Profiles*), + void (*)(Profiles*), + void (*)(Profiles*, quintptr, quintptr), + void (*)(Profiles*), + void (*)(Profiles*), + void (*)(Profiles*, int, int), + void (*)(Profiles*), + void (*)(Profiles*, int, int, int), + void (*)(Profiles*), + void (*)(Profiles*, int, int), + void (*)(Profiles*), Settings*, void (*)(Settings*), void (*)(Settings*), void (*)(Settings*), void (*)(PasswordMaker*), void (*)(PasswordMaker*)); + void password_maker_free(PasswordMaker::Private*); + void password_maker_generated_password_get(const PasswordMaker::Private*, QString*, qstring_set); + quint8 password_maker_generator_state_get(const PasswordMaker::Private*); + bool password_maker_i_say_sexy_things_to_myself_while_im_dancing_get(const PasswordMaker::Private*); + void password_maker_i_say_sexy_things_to_myself_while_im_dancing_set(const PasswordMaker::Private*, bool); + void password_maker_master_password_get(const PasswordMaker::Private*, QString*, qstring_set); + void password_maker_master_password_set(const PasswordMaker::Private*, const ushort *str, int len); + const Profiles::Private* password_maker_profiles_get(const PasswordMaker::Private*); + const Settings::Private* password_maker_settings_get(const PasswordMaker::Private*); + void password_maker_url_get(const PasswordMaker::Private*, QString*, qstring_set); + void password_maker_url_set(const PasswordMaker::Private*, const ushort *str, int len); + void password_maker_used_text_get(const PasswordMaker::Private*, QString*, qstring_set); + void password_maker_used_text_set(const PasswordMaker::Private*, const ushort *str, int len); + void password_maker_profile_changed(const PasswordMaker::Private*); + bool password_maker_store_settings(const PasswordMaker::Private*); +}; + +extern "C" { + void profiles_data_characters(const Profiles::Private*, int, QString*, qstring_set); + bool profiles_set_data_characters(const Profiles::Private*, int, const ushort* s, int len); + quint8 profiles_data_hash_algorithm(const Profiles::Private*, int); + bool profiles_set_data_hash_algorithm(const Profiles::Private*, int, quint8); + quint8 profiles_data_leet_level(const Profiles::Private*, int); + bool profiles_set_data_leet_level(const Profiles::Private*, int, quint8); + void profiles_data_modifier(const Profiles::Private*, int, QString*, qstring_set); + bool profiles_set_data_modifier(const Profiles::Private*, int, const ushort* s, int len); + void profiles_data_name(const Profiles::Private*, int, QString*, qstring_set); + bool profiles_set_data_name(const Profiles::Private*, int, const ushort* s, int len); + quint32 profiles_data_password_length(const Profiles::Private*, int); + bool profiles_set_data_password_length(const Profiles::Private*, int, quint32); + void profiles_data_prefix(const Profiles::Private*, int, QString*, qstring_set); + bool profiles_set_data_prefix(const Profiles::Private*, int, const ushort* s, int len); + void profiles_data_suffix(const Profiles::Private*, int, QString*, qstring_set); + bool profiles_set_data_suffix(const Profiles::Private*, int, const ushort* s, int len); + bool profiles_data_use_domain(const Profiles::Private*, int); + bool profiles_set_data_use_domain(const Profiles::Private*, int, bool); + quint8 profiles_data_use_leet(const Profiles::Private*, int); + bool profiles_set_data_use_leet(const Profiles::Private*, int, quint8); + bool profiles_data_use_port_path(const Profiles::Private*, int); + bool profiles_set_data_use_port_path(const Profiles::Private*, int, bool); + bool profiles_data_use_protocol(const Profiles::Private*, int); + bool profiles_set_data_use_protocol(const Profiles::Private*, int, bool); + bool profiles_data_use_subdomains(const Profiles::Private*, int); + bool profiles_set_data_use_subdomains(const Profiles::Private*, int, bool); + bool profiles_data_use_undefined_as_protocol_fallback(const Profiles::Private*, int); + bool profiles_set_data_use_undefined_as_protocol_fallback(const Profiles::Private*, int, bool); + bool profiles_data_use_user_info(const Profiles::Private*, int); + bool profiles_set_data_use_user_info(const Profiles::Private*, int, bool); + void profiles_data_username(const Profiles::Private*, int, QString*, qstring_set); + bool profiles_set_data_username(const Profiles::Private*, int, const ushort* s, int len); + void profiles_sort(const Profiles::Private*, unsigned char column, Qt::SortOrder order = Qt::AscendingOrder); + + int profiles_row_count(const Profiles::Private*); + bool profiles_insert_rows(const Profiles::Private*, int, int); + bool profiles_remove_rows(const Profiles::Private*, int, int); + bool profiles_can_fetch_more(const Profiles::Private*); + void profiles_fetch_more(const Profiles::Private*); +} +int Profiles::columnCount(const QModelIndex &parent) const +{ + return (parent.isValid()) ? 0 : 16; +} + +bool Profiles::hasChildren(const QModelIndex &parent) const +{ + return rowCount(parent) > 0; +} + +int Profiles::rowCount(const QModelIndex &parent) const +{ + return (parent.isValid()) ? 0 : profiles_row_count(m_d); +} + +bool Profiles::insertRows(int row, int count, const QModelIndex &) +{ + return profiles_insert_rows(m_d, row, count); +} + +bool Profiles::removeRows(int row, int count, const QModelIndex &) +{ + return profiles_remove_rows(m_d, row, count); +} + +QModelIndex Profiles::index(int row, int column, const QModelIndex &parent) const +{ + if (!parent.isValid() && row >= 0 && row < rowCount(parent) && column >= 0 && column < 16) { + return createIndex(row, column, (quintptr)row); + } + return QModelIndex(); +} + +QModelIndex Profiles::parent(const QModelIndex &) const +{ + return QModelIndex(); +} + +bool Profiles::canFetchMore(const QModelIndex &parent) const +{ + return (parent.isValid()) ? 0 : profiles_can_fetch_more(m_d); +} + +void Profiles::fetchMore(const QModelIndex &parent) +{ + if (!parent.isValid()) { + profiles_fetch_more(m_d); + } +} +void Profiles::updatePersistentIndexes() {} + +void Profiles::sort(int column, Qt::SortOrder order) +{ + profiles_sort(m_d, column, order); +} +Qt::ItemFlags Profiles::flags(const QModelIndex &i) const +{ + auto flags = QAbstractItemModel::flags(i); + if (i.column() == 0) { + flags |= Qt::ItemIsEditable; + } + if (i.column() == 1) { + flags |= Qt::ItemIsEditable; + } + if (i.column() == 2) { + flags |= Qt::ItemIsEditable; + } + if (i.column() == 3) { + flags |= Qt::ItemIsEditable; + } + if (i.column() == 4) { + flags |= Qt::ItemIsEditable; + } + if (i.column() == 5) { + flags |= Qt::ItemIsEditable; + } + if (i.column() == 6) { + flags |= Qt::ItemIsEditable; + } + if (i.column() == 7) { + flags |= Qt::ItemIsEditable; + } + if (i.column() == 8) { + flags |= Qt::ItemIsEditable; + } + if (i.column() == 9) { + flags |= Qt::ItemIsEditable; + } + if (i.column() == 10) { + flags |= Qt::ItemIsEditable; + } + if (i.column() == 11) { + flags |= Qt::ItemIsEditable; + } + if (i.column() == 12) { + flags |= Qt::ItemIsEditable; + } + if (i.column() == 13) { + flags |= Qt::ItemIsEditable; + } + if (i.column() == 14) { + flags |= Qt::ItemIsEditable; + } + if (i.column() == 15) { + flags |= Qt::ItemIsEditable; + } + return flags; +} + +QString Profiles::characters(int row) const +{ + QString s; + profiles_data_characters(m_d, row, &s, set_qstring); + return s; +} + +bool Profiles::setCharacters(int row, const QString& value) +{ + bool set = false; + set = profiles_set_data_characters(m_d, row, value.utf16(), value.length()); + if (set) { + QModelIndex index = createIndex(row, 0, row); + Q_EMIT dataChanged(index, index); + } + return set; +} + +quint8 Profiles::hash_algorithm(int row) const +{ + return profiles_data_hash_algorithm(m_d, row); +} + +bool Profiles::setHash_algorithm(int row, quint8 value) +{ + bool set = false; + set = profiles_set_data_hash_algorithm(m_d, row, value); + if (set) { + QModelIndex index = createIndex(row, 0, row); + Q_EMIT dataChanged(index, index); + } + return set; +} + +quint8 Profiles::leet_level(int row) const +{ + return profiles_data_leet_level(m_d, row); +} + +bool Profiles::setLeet_level(int row, quint8 value) +{ + bool set = false; + set = profiles_set_data_leet_level(m_d, row, value); + if (set) { + QModelIndex index = createIndex(row, 0, row); + Q_EMIT dataChanged(index, index); + } + return set; +} + +QString Profiles::modifier(int row) const +{ + QString s; + profiles_data_modifier(m_d, row, &s, set_qstring); + return s; +} + +bool Profiles::setModifier(int row, const QString& value) +{ + bool set = false; + set = profiles_set_data_modifier(m_d, row, value.utf16(), value.length()); + if (set) { + QModelIndex index = createIndex(row, 0, row); + Q_EMIT dataChanged(index, index); + } + return set; +} + +QString Profiles::name(int row) const +{ + QString s; + profiles_data_name(m_d, row, &s, set_qstring); + return s; +} + +bool Profiles::setName(int row, const QString& value) +{ + bool set = false; + set = profiles_set_data_name(m_d, row, value.utf16(), value.length()); + if (set) { + QModelIndex index = createIndex(row, 0, row); + Q_EMIT dataChanged(index, index); + } + return set; +} + +quint32 Profiles::password_length(int row) const +{ + return profiles_data_password_length(m_d, row); +} + +bool Profiles::setPassword_length(int row, quint32 value) +{ + bool set = false; + set = profiles_set_data_password_length(m_d, row, value); + if (set) { + QModelIndex index = createIndex(row, 0, row); + Q_EMIT dataChanged(index, index); + } + return set; +} + +QString Profiles::prefix(int row) const +{ + QString s; + profiles_data_prefix(m_d, row, &s, set_qstring); + return s; +} + +bool Profiles::setPrefix(int row, const QString& value) +{ + bool set = false; + set = profiles_set_data_prefix(m_d, row, value.utf16(), value.length()); + if (set) { + QModelIndex index = createIndex(row, 0, row); + Q_EMIT dataChanged(index, index); + } + return set; +} + +QString Profiles::suffix(int row) const +{ + QString s; + profiles_data_suffix(m_d, row, &s, set_qstring); + return s; +} + +bool Profiles::setSuffix(int row, const QString& value) +{ + bool set = false; + set = profiles_set_data_suffix(m_d, row, value.utf16(), value.length()); + if (set) { + QModelIndex index = createIndex(row, 0, row); + Q_EMIT dataChanged(index, index); + } + return set; +} + +bool Profiles::use_domain(int row) const +{ + return profiles_data_use_domain(m_d, row); +} + +bool Profiles::setUse_domain(int row, bool value) +{ + bool set = false; + set = profiles_set_data_use_domain(m_d, row, value); + if (set) { + QModelIndex index = createIndex(row, 0, row); + Q_EMIT dataChanged(index, index); + } + return set; +} + +quint8 Profiles::use_leet(int row) const +{ + return profiles_data_use_leet(m_d, row); +} + +bool Profiles::setUse_leet(int row, quint8 value) +{ + bool set = false; + set = profiles_set_data_use_leet(m_d, row, value); + if (set) { + QModelIndex index = createIndex(row, 0, row); + Q_EMIT dataChanged(index, index); + } + return set; +} + +bool Profiles::use_port_path(int row) const +{ + return profiles_data_use_port_path(m_d, row); +} + +bool Profiles::setUse_port_path(int row, bool value) +{ + bool set = false; + set = profiles_set_data_use_port_path(m_d, row, value); + if (set) { + QModelIndex index = createIndex(row, 0, row); + Q_EMIT dataChanged(index, index); + } + return set; +} + +bool Profiles::use_protocol(int row) const +{ + return profiles_data_use_protocol(m_d, row); +} + +bool Profiles::setUse_protocol(int row, bool value) +{ + bool set = false; + set = profiles_set_data_use_protocol(m_d, row, value); + if (set) { + QModelIndex index = createIndex(row, 0, row); + Q_EMIT dataChanged(index, index); + } + return set; +} + +bool Profiles::use_subdomains(int row) const +{ + return profiles_data_use_subdomains(m_d, row); +} + +bool Profiles::setUse_subdomains(int row, bool value) +{ + bool set = false; + set = profiles_set_data_use_subdomains(m_d, row, value); + if (set) { + QModelIndex index = createIndex(row, 0, row); + Q_EMIT dataChanged(index, index); + } + return set; +} + +bool Profiles::use_undefined_as_protocol_fallback(int row) const +{ + return profiles_data_use_undefined_as_protocol_fallback(m_d, row); +} + +bool Profiles::setUse_undefined_as_protocol_fallback(int row, bool value) +{ + bool set = false; + set = profiles_set_data_use_undefined_as_protocol_fallback(m_d, row, value); + if (set) { + QModelIndex index = createIndex(row, 0, row); + Q_EMIT dataChanged(index, index); + } + return set; +} + +bool Profiles::use_user_info(int row) const +{ + return profiles_data_use_user_info(m_d, row); +} + +bool Profiles::setUse_user_info(int row, bool value) +{ + bool set = false; + set = profiles_set_data_use_user_info(m_d, row, value); + if (set) { + QModelIndex index = createIndex(row, 0, row); + Q_EMIT dataChanged(index, index); + } + return set; +} + +QString Profiles::username(int row) const +{ + QString s; + profiles_data_username(m_d, row, &s, set_qstring); + return s; +} + +bool Profiles::setUsername(int row, const QString& value) +{ + bool set = false; + set = profiles_set_data_username(m_d, row, value.utf16(), value.length()); + if (set) { + QModelIndex index = createIndex(row, 0, row); + Q_EMIT dataChanged(index, index); + } + return set; +} + +QVariant Profiles::data(const QModelIndex &index, int role) const +{ + Q_ASSERT(rowCount(index.parent()) > index.row()); + switch (index.column()) { + case 0: + switch (role) { + case Qt::UserRole + 0: + return QVariant::fromValue(characters(index.row())); + case Qt::UserRole + 1: + return QVariant::fromValue(hash_algorithm(index.row())); + case Qt::UserRole + 2: + return QVariant::fromValue(leet_level(index.row())); + case Qt::UserRole + 3: + return QVariant::fromValue(modifier(index.row())); + case Qt::DisplayRole: + case Qt::EditRole: + case Qt::UserRole + 4: + return QVariant::fromValue(name(index.row())); + case Qt::UserRole + 5: + return QVariant::fromValue(password_length(index.row())); + case Qt::UserRole + 6: + return QVariant::fromValue(prefix(index.row())); + case Qt::UserRole + 7: + return QVariant::fromValue(suffix(index.row())); + case Qt::UserRole + 8: + return QVariant::fromValue(use_domain(index.row())); + case Qt::UserRole + 9: + return QVariant::fromValue(use_leet(index.row())); + case Qt::UserRole + 10: + return QVariant::fromValue(use_port_path(index.row())); + case Qt::UserRole + 11: + return QVariant::fromValue(use_protocol(index.row())); + case Qt::UserRole + 12: + return QVariant::fromValue(use_subdomains(index.row())); + case Qt::UserRole + 13: + return QVariant::fromValue(use_undefined_as_protocol_fallback(index.row())); + case Qt::UserRole + 14: + return QVariant::fromValue(use_user_info(index.row())); + case Qt::UserRole + 15: + return QVariant::fromValue(username(index.row())); + } + break; + case 1: + switch (role) { + case Qt::DisplayRole: + case Qt::EditRole: + case Qt::UserRole + 11: + return QVariant::fromValue(use_protocol(index.row())); + } + break; + case 2: + switch (role) { + case Qt::DisplayRole: + case Qt::EditRole: + case Qt::UserRole + 12: + return QVariant::fromValue(use_subdomains(index.row())); + } + break; + case 3: + switch (role) { + case Qt::DisplayRole: + case Qt::EditRole: + case Qt::UserRole + 8: + return QVariant::fromValue(use_domain(index.row())); + } + break; + case 4: + switch (role) { + case Qt::DisplayRole: + case Qt::EditRole: + case Qt::UserRole + 10: + return QVariant::fromValue(use_port_path(index.row())); + } + break; + case 5: + switch (role) { + case Qt::DisplayRole: + case Qt::EditRole: + case Qt::UserRole + 14: + return QVariant::fromValue(use_user_info(index.row())); + } + break; + case 6: + switch (role) { + case Qt::DisplayRole: + case Qt::EditRole: + case Qt::UserRole + 13: + return QVariant::fromValue(use_undefined_as_protocol_fallback(index.row())); + } + break; + case 7: + switch (role) { + case Qt::DisplayRole: + case Qt::EditRole: + case Qt::UserRole + 5: + return QVariant::fromValue(password_length(index.row())); + } + break; + case 8: + switch (role) { + case Qt::DisplayRole: + case Qt::EditRole: + case Qt::UserRole + 1: + return QVariant::fromValue(hash_algorithm(index.row())); + } + break; + case 9: + switch (role) { + case Qt::DisplayRole: + case Qt::EditRole: + case Qt::UserRole + 9: + return QVariant::fromValue(use_leet(index.row())); + } + break; + case 10: + switch (role) { + case Qt::DisplayRole: + case Qt::EditRole: + case Qt::UserRole + 2: + return QVariant::fromValue(leet_level(index.row())); + } + break; + case 11: + switch (role) { + case Qt::DisplayRole: + case Qt::EditRole: + case Qt::UserRole + 0: + return QVariant::fromValue(characters(index.row())); + } + break; + case 12: + switch (role) { + case Qt::DisplayRole: + case Qt::EditRole: + case Qt::UserRole + 15: + return QVariant::fromValue(username(index.row())); + } + break; + case 13: + switch (role) { + case Qt::DisplayRole: + case Qt::EditRole: + case Qt::UserRole + 3: + return QVariant::fromValue(modifier(index.row())); + } + break; + case 14: + switch (role) { + case Qt::DisplayRole: + case Qt::EditRole: + case Qt::UserRole + 6: + return QVariant::fromValue(prefix(index.row())); + } + break; + case 15: + switch (role) { + case Qt::DisplayRole: + case Qt::EditRole: + case Qt::UserRole + 7: + return QVariant::fromValue(suffix(index.row())); + } + break; + } + return QVariant(); +} + +int Profiles::role(const char* name) const { + auto names = roleNames(); + auto i = names.constBegin(); + while (i != names.constEnd()) { + if (i.value() == name) { + return i.key(); + } + ++i; + } + return -1; +} +QHash<int, QByteArray> Profiles::roleNames() const { + QHash<int, QByteArray> names = QAbstractItemModel::roleNames(); + names.insert(Qt::UserRole + 0, "characters"); + names.insert(Qt::UserRole + 1, "hash_algorithm"); + names.insert(Qt::UserRole + 2, "leet_level"); + names.insert(Qt::UserRole + 3, "modifier"); + names.insert(Qt::UserRole + 4, "name"); + names.insert(Qt::UserRole + 5, "password_length"); + names.insert(Qt::UserRole + 6, "prefix"); + names.insert(Qt::UserRole + 7, "suffix"); + names.insert(Qt::UserRole + 8, "use_domain"); + names.insert(Qt::UserRole + 9, "use_leet"); + names.insert(Qt::UserRole + 10, "use_port_path"); + names.insert(Qt::UserRole + 11, "use_protocol"); + names.insert(Qt::UserRole + 12, "use_subdomains"); + names.insert(Qt::UserRole + 13, "use_undefined_as_protocol_fallback"); + names.insert(Qt::UserRole + 14, "use_user_info"); + names.insert(Qt::UserRole + 15, "username"); + return names; +} +QVariant Profiles::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation != Qt::Horizontal) { + return QVariant(); + } + return m_headerData.value(qMakePair(section, (Qt::ItemDataRole)role), role == Qt::DisplayRole ?QString::number(section + 1) :QVariant()); +} + +bool Profiles::setHeaderData(int section, Qt::Orientation orientation, const QVariant &value, int role) +{ + if (orientation != Qt::Horizontal) { + return false; + } + m_headerData.insert(qMakePair(section, (Qt::ItemDataRole)role), value); + return true; +} + +bool Profiles::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (index.column() == 0) { + if (role == Qt::UserRole + 0) { + if (value.canConvert(qMetaTypeId<QString>())) { + return setCharacters(index.row(), value.value<QString>()); + } + } + if (role == Qt::UserRole + 1) { + if (value.canConvert(qMetaTypeId<quint8>())) { + return setHash_algorithm(index.row(), value.value<quint8>()); + } + } + if (role == Qt::UserRole + 2) { + if (value.canConvert(qMetaTypeId<quint8>())) { + return setLeet_level(index.row(), value.value<quint8>()); + } + } + if (role == Qt::UserRole + 3) { + if (value.canConvert(qMetaTypeId<QString>())) { + return setModifier(index.row(), value.value<QString>()); + } + } + if (role == Qt::DisplayRole || role == Qt::EditRole || role == Qt::UserRole + 4) { + if (value.canConvert(qMetaTypeId<QString>())) { + return setName(index.row(), value.value<QString>()); + } + } + if (role == Qt::UserRole + 5) { + if (value.canConvert(qMetaTypeId<quint32>())) { + return setPassword_length(index.row(), value.value<quint32>()); + } + } + if (role == Qt::UserRole + 6) { + if (value.canConvert(qMetaTypeId<QString>())) { + return setPrefix(index.row(), value.value<QString>()); + } + } + if (role == Qt::UserRole + 7) { + if (value.canConvert(qMetaTypeId<QString>())) { + return setSuffix(index.row(), value.value<QString>()); + } + } + if (role == Qt::UserRole + 8) { + if (value.canConvert(qMetaTypeId<bool>())) { + return setUse_domain(index.row(), value.value<bool>()); + } + } + if (role == Qt::UserRole + 9) { + if (value.canConvert(qMetaTypeId<quint8>())) { + return setUse_leet(index.row(), value.value<quint8>()); + } + } + if (role == Qt::UserRole + 10) { + if (value.canConvert(qMetaTypeId<bool>())) { + return setUse_port_path(index.row(), value.value<bool>()); + } + } + if (role == Qt::UserRole + 11) { + if (value.canConvert(qMetaTypeId<bool>())) { + return setUse_protocol(index.row(), value.value<bool>()); + } + } + if (role == Qt::UserRole + 12) { + if (value.canConvert(qMetaTypeId<bool>())) { + return setUse_subdomains(index.row(), value.value<bool>()); + } + } + if (role == Qt::UserRole + 13) { + if (value.canConvert(qMetaTypeId<bool>())) { + return setUse_undefined_as_protocol_fallback(index.row(), value.value<bool>()); + } + } + if (role == Qt::UserRole + 14) { + if (value.canConvert(qMetaTypeId<bool>())) { + return setUse_user_info(index.row(), value.value<bool>()); + } + } + if (role == Qt::UserRole + 15) { + if (value.canConvert(qMetaTypeId<QString>())) { + return setUsername(index.row(), value.value<QString>()); + } + } + } + if (index.column() == 1) { + if (role == Qt::DisplayRole || role == Qt::EditRole || role == Qt::UserRole + 11) { + if (value.canConvert(qMetaTypeId<bool>())) { + return setUse_protocol(index.row(), value.value<bool>()); + } + } + } + if (index.column() == 2) { + if (role == Qt::DisplayRole || role == Qt::EditRole || role == Qt::UserRole + 12) { + if (value.canConvert(qMetaTypeId<bool>())) { + return setUse_subdomains(index.row(), value.value<bool>()); + } + } + } + if (index.column() == 3) { + if (role == Qt::DisplayRole || role == Qt::EditRole || role == Qt::UserRole + 8) { + if (value.canConvert(qMetaTypeId<bool>())) { + return setUse_domain(index.row(), value.value<bool>()); + } + } + } + if (index.column() == 4) { + if (role == Qt::DisplayRole || role == Qt::EditRole || role == Qt::UserRole + 10) { + if (value.canConvert(qMetaTypeId<bool>())) { + return setUse_port_path(index.row(), value.value<bool>()); + } + } + } + if (index.column() == 5) { + if (role == Qt::DisplayRole || role == Qt::EditRole || role == Qt::UserRole + 14) { + if (value.canConvert(qMetaTypeId<bool>())) { + return setUse_user_info(index.row(), value.value<bool>()); + } + } + } + if (index.column() == 6) { + if (role == Qt::DisplayRole || role == Qt::EditRole || role == Qt::UserRole + 13) { + if (value.canConvert(qMetaTypeId<bool>())) { + return setUse_undefined_as_protocol_fallback(index.row(), value.value<bool>()); + } + } + } + if (index.column() == 7) { + if (role == Qt::DisplayRole || role == Qt::EditRole || role == Qt::UserRole + 5) { + if (value.canConvert(qMetaTypeId<quint32>())) { + return setPassword_length(index.row(), value.value<quint32>()); + } + } + } + if (index.column() == 8) { + if (role == Qt::DisplayRole || role == Qt::EditRole || role == Qt::UserRole + 1) { + if (value.canConvert(qMetaTypeId<quint8>())) { + return setHash_algorithm(index.row(), value.value<quint8>()); + } + } + } + if (index.column() == 9) { + if (role == Qt::DisplayRole || role == Qt::EditRole || role == Qt::UserRole + 9) { + if (value.canConvert(qMetaTypeId<quint8>())) { + return setUse_leet(index.row(), value.value<quint8>()); + } + } + } + if (index.column() == 10) { + if (role == Qt::DisplayRole || role == Qt::EditRole || role == Qt::UserRole + 2) { + if (value.canConvert(qMetaTypeId<quint8>())) { + return setLeet_level(index.row(), value.value<quint8>()); + } + } + } + if (index.column() == 11) { + if (role == Qt::DisplayRole || role == Qt::EditRole || role == Qt::UserRole + 0) { + if (value.canConvert(qMetaTypeId<QString>())) { + return setCharacters(index.row(), value.value<QString>()); + } + } + } + if (index.column() == 12) { + if (role == Qt::DisplayRole || role == Qt::EditRole || role == Qt::UserRole + 15) { + if (value.canConvert(qMetaTypeId<QString>())) { + return setUsername(index.row(), value.value<QString>()); + } + } + } + if (index.column() == 13) { + if (role == Qt::DisplayRole || role == Qt::EditRole || role == Qt::UserRole + 3) { + if (value.canConvert(qMetaTypeId<QString>())) { + return setModifier(index.row(), value.value<QString>()); + } + } + } + if (index.column() == 14) { + if (role == Qt::DisplayRole || role == Qt::EditRole || role == Qt::UserRole + 6) { + if (value.canConvert(qMetaTypeId<QString>())) { + return setPrefix(index.row(), value.value<QString>()); + } + } + } + if (index.column() == 15) { + if (role == Qt::DisplayRole || role == Qt::EditRole || role == Qt::UserRole + 7) { + if (value.canConvert(qMetaTypeId<QString>())) { + return setSuffix(index.row(), value.value<QString>()); + } + } + } + return false; +} + +extern "C" { + Profiles::Private* profiles_new(Profiles*, void (*)(Profiles*), void (*)(Profiles*), + void (*)(const Profiles*), + void (*)(Profiles*), + void (*)(Profiles*), + void (*)(Profiles*, quintptr, quintptr), + void (*)(Profiles*), + void (*)(Profiles*), + void (*)(Profiles*, int, int), + void (*)(Profiles*), + void (*)(Profiles*, int, int, int), + void (*)(Profiles*), + void (*)(Profiles*, int, int), + void (*)(Profiles*)); + void profiles_free(Profiles::Private*); + quint32 profiles_current_profile_get(const Profiles::Private*); + void profiles_current_profile_set(const Profiles::Private*, quint32); + void profiles_current_profile_name_get(const Profiles::Private*, QString*, qstring_set); + bool profiles_store(const Profiles::Private*); +}; + +extern "C" { + Settings::Private* settings_new(Settings*, void (*)(Settings*), void (*)(Settings*), void (*)(Settings*)); + void settings_free(Settings::Private*); + option_quint32 settings_clear_generated_password_seconds_get(const Settings::Private*); + void settings_clear_generated_password_seconds_set(const Settings::Private*, quint32); + void settings_clear_generated_password_seconds_set_none(const Settings::Private*); + option_quint32 settings_clear_master_password_seconds_get(const Settings::Private*); + void settings_clear_master_password_seconds_set(const Settings::Private*, quint32); + void settings_clear_master_password_seconds_set_none(const Settings::Private*); + bool settings_hide_generated_password_get(const Settings::Private*); + void settings_hide_generated_password_set(const Settings::Private*, bool); +}; + +PasswordMaker::PasswordMaker(bool /*owned*/, QObject *parent): + QObject(parent), + m_profiles(new Profiles(false, this)), + m_settings(new Settings(false, this)), + m_d(nullptr), + m_d_owned(nullptr) +{ +} + +PasswordMaker::PasswordMaker(QObject *parent): + QObject(parent), + m_profiles(new Profiles(false, this)), + m_settings(new Settings(false, this)), + m_d(nullptr), + m_d_owned(password_maker_new(this, + passwordMakerGenerated_passwordChanged, + passwordMakerGenerator_stateChanged, + passwordMakerI_say_sexy_things_to_myself_while_im_dancingChanged, + passwordMakerMaster_passwordChanged, m_profiles, + profilesCurrent_profileChanged, + profilesCurrent_profile_nameChanged, + [](const Profiles* o) { + Q_EMIT o->newDataReady(QVariant()); + }, + [](Profiles* o) { + Q_EMIT o->layoutAboutToBeChanged(); + }, + [](Profiles* o) { + o->updatePersistentIndexes(); + Q_EMIT o->layoutChanged(); + }, + [](Profiles* o, quintptr first, quintptr last) { + o->dataChanged(o->createIndex(first, 0, first), + o->createIndex(last, 15, last)); + }, + [](Profiles* o) { + o->beginResetModel(); + }, + [](Profiles* o) { + o->endResetModel(); + }, + [](Profiles* o, int first, int last) { + o->beginInsertRows(QModelIndex(), first, last); + }, + [](Profiles* o) { + o->endInsertRows(); + }, + [](Profiles* o, int first, int last, int destination) { + o->beginMoveRows(QModelIndex(), first, last, QModelIndex(), destination); + }, + [](Profiles* o) { + o->endMoveRows(); + }, + [](Profiles* o, int first, int last) { + o->beginRemoveRows(QModelIndex(), first, last); + }, + [](Profiles* o) { + o->endRemoveRows(); + } +, m_settings, + settingsClear_generated_password_secondsChanged, + settingsClear_master_password_secondsChanged, + settingsHide_generated_passwordChanged, + passwordMakerUrlChanged, + passwordMakerUsed_textChanged)) +{ + m_d = m_d_owned; + m_profiles->m_d = password_maker_profiles_get(m_d); + m_settings->m_d = password_maker_settings_get(m_d); + connect(this->m_profiles, &Profiles::newDataReady, this->m_profiles, [this](const QVariant& v) { + this->m_profiles->fetchMore(QModelIndex()); + }, Qt::QueuedConnection); +} + +PasswordMaker::~PasswordMaker() { + if (m_d_owned) { + password_maker_free(m_d_owned); + } +} +QString PasswordMaker::generated_password() const +{ + QString v; + password_maker_generated_password_get(m_d, &v, set_qstring); + return v; +} +quint8 PasswordMaker::generator_state() const +{ + return password_maker_generator_state_get(m_d); +} +bool PasswordMaker::i_say_sexy_things_to_myself_while_im_dancing() const +{ + return password_maker_i_say_sexy_things_to_myself_while_im_dancing_get(m_d); +} +void PasswordMaker::setI_say_sexy_things_to_myself_while_im_dancing(bool v) { + password_maker_i_say_sexy_things_to_myself_while_im_dancing_set(m_d, v); +} +QString PasswordMaker::master_password() const +{ + QString v; + password_maker_master_password_get(m_d, &v, set_qstring); + return v; +} +void PasswordMaker::setMaster_password(const QString& v) { + password_maker_master_password_set(m_d, reinterpret_cast<const ushort*>(v.data()), v.size()); +} +const Profiles* PasswordMaker::profiles() const +{ + return m_profiles; +} +Profiles* PasswordMaker::profiles() +{ + return m_profiles; +} +const Settings* PasswordMaker::settings() const +{ + return m_settings; +} +Settings* PasswordMaker::settings() +{ + return m_settings; +} +QString PasswordMaker::url() const +{ + QString v; + password_maker_url_get(m_d, &v, set_qstring); + return v; +} +void PasswordMaker::setUrl(const QString& v) { + password_maker_url_set(m_d, reinterpret_cast<const ushort*>(v.data()), v.size()); +} +QString PasswordMaker::used_text() const +{ + QString v; + password_maker_used_text_get(m_d, &v, set_qstring); + return v; +} +void PasswordMaker::setUsed_text(const QString& v) { + password_maker_used_text_set(m_d, reinterpret_cast<const ushort*>(v.data()), v.size()); +} +void PasswordMaker::profile_changed() const +{ + return password_maker_profile_changed(m_d); +} +bool PasswordMaker::store_settings() const +{ + return password_maker_store_settings(m_d); +} +Profiles::Profiles(bool /*owned*/, QObject *parent): + QAbstractItemModel(parent), + m_d(nullptr), + m_d_owned(nullptr) +{ + initHeaderData(); +} + +Profiles::Profiles(QObject *parent): + QAbstractItemModel(parent), + m_d(nullptr), + m_d_owned(profiles_new(this, + profilesCurrent_profileChanged, + profilesCurrent_profile_nameChanged, + [](const Profiles* o) { + Q_EMIT o->newDataReady(QVariant()); + }, + [](Profiles* o) { + Q_EMIT o->layoutAboutToBeChanged(); + }, + [](Profiles* o) { + o->updatePersistentIndexes(); + Q_EMIT o->layoutChanged(); + }, + [](Profiles* o, quintptr first, quintptr last) { + o->dataChanged(o->createIndex(first, 0, first), + o->createIndex(last, 15, last)); + }, + [](Profiles* o) { + o->beginResetModel(); + }, + [](Profiles* o) { + o->endResetModel(); + }, + [](Profiles* o, int first, int last) { + o->beginInsertRows(QModelIndex(), first, last); + }, + [](Profiles* o) { + o->endInsertRows(); + }, + [](Profiles* o, int first, int last, int destination) { + o->beginMoveRows(QModelIndex(), first, last, QModelIndex(), destination); + }, + [](Profiles* o) { + o->endMoveRows(); + }, + [](Profiles* o, int first, int last) { + o->beginRemoveRows(QModelIndex(), first, last); + }, + [](Profiles* o) { + o->endRemoveRows(); + } +)) +{ + m_d = m_d_owned; + connect(this, &Profiles::newDataReady, this, [this](const QVariant& v) { + this->fetchMore(QModelIndex()); + }, Qt::QueuedConnection); + initHeaderData(); +} + +Profiles::~Profiles() { + if (m_d_owned) { + profiles_free(m_d_owned); + } +} +void Profiles::initHeaderData() { + m_headerData.insert(qMakePair(0, Qt::DisplayRole), QVariant("name")); + m_headerData.insert(qMakePair(1, Qt::DisplayRole), QVariant("use_protocol")); + m_headerData.insert(qMakePair(2, Qt::DisplayRole), QVariant("use_subdomains")); + m_headerData.insert(qMakePair(3, Qt::DisplayRole), QVariant("use_domain")); + m_headerData.insert(qMakePair(4, Qt::DisplayRole), QVariant("use_port_path")); + m_headerData.insert(qMakePair(5, Qt::DisplayRole), QVariant("use_user_info")); + m_headerData.insert(qMakePair(6, Qt::DisplayRole), QVariant("use_undefined_as_protocol_fallback")); + m_headerData.insert(qMakePair(7, Qt::DisplayRole), QVariant("password_length")); + m_headerData.insert(qMakePair(8, Qt::DisplayRole), QVariant("hash_algorithm")); + m_headerData.insert(qMakePair(9, Qt::DisplayRole), QVariant("use_leet")); + m_headerData.insert(qMakePair(10, Qt::DisplayRole), QVariant("leet_level")); + m_headerData.insert(qMakePair(11, Qt::DisplayRole), QVariant("characters")); + m_headerData.insert(qMakePair(12, Qt::DisplayRole), QVariant("username")); + m_headerData.insert(qMakePair(13, Qt::DisplayRole), QVariant("modifier")); + m_headerData.insert(qMakePair(14, Qt::DisplayRole), QVariant("prefix")); + m_headerData.insert(qMakePair(15, Qt::DisplayRole), QVariant("suffix")); +} +quint32 Profiles::current_profile() const +{ + return profiles_current_profile_get(m_d); +} +void Profiles::setCurrent_profile(quint32 v) { + profiles_current_profile_set(m_d, v); +} +QString Profiles::current_profile_name() const +{ + QString v; + profiles_current_profile_name_get(m_d, &v, set_qstring); + return v; +} +bool Profiles::store() const +{ + return profiles_store(m_d); +} +Settings::Settings(bool /*owned*/, QObject *parent): + QObject(parent), + m_d(nullptr), + m_d_owned(nullptr) +{ +} + +Settings::Settings(QObject *parent): + QObject(parent), + m_d(nullptr), + m_d_owned(settings_new(this, + settingsClear_generated_password_secondsChanged, + settingsClear_master_password_secondsChanged, + settingsHide_generated_passwordChanged)) +{ + m_d = m_d_owned; +} + +Settings::~Settings() { + if (m_d_owned) { + settings_free(m_d_owned); + } +} +QVariant Settings::clear_generated_password_seconds() const +{ + QVariant v; + auto r = settings_clear_generated_password_seconds_get(m_d); + if (r.some) { + v.setValue(r.value); + } + return r; +} +void Settings::setClear_generated_password_seconds(const QVariant& v) { + if (v.isNull() || !v.canConvert<quint32>()) { + settings_clear_generated_password_seconds_set_none(m_d); + } else { + settings_clear_generated_password_seconds_set(m_d, v.value<quint32>()); + } +} +QVariant Settings::clear_master_password_seconds() const +{ + QVariant v; + auto r = settings_clear_master_password_seconds_get(m_d); + if (r.some) { + v.setValue(r.value); + } + return r; +} +void Settings::setClear_master_password_seconds(const QVariant& v) { + if (v.isNull() || !v.canConvert<quint32>()) { + settings_clear_master_password_seconds_set_none(m_d); + } else { + settings_clear_master_password_seconds_set(m_d, v.value<quint32>()); + } +} +bool Settings::hide_generated_password() const +{ + return settings_hide_generated_password_get(m_d); +} +void Settings::setHide_generated_password(bool v) { + settings_hide_generated_password_set(m_d, v); +} diff --git a/src/Bindings.h b/src/Bindings.h new file mode 100644 index 0000000..3aa3e77 --- /dev/null +++ b/src/Bindings.h @@ -0,0 +1,180 @@ +/* generated by rust_qt_binding_generator */ +/* Beware: a lot of const pointers mentioned in this file are mutating data on the Rust side. */ +#ifndef BINDINGS_H +#define BINDINGS_H + +#include <QtCore/QObject> +#include <QtCore/QAbstractItemModel> + +class PasswordMaker; +class Profiles; +class Settings; + +class PasswordMaker : public QObject +{ + Q_OBJECT +public: + class Private; +private: + Profiles* const m_profiles; + Settings* const m_settings; + const Private * m_d; + Private * const m_d_owned; + Q_PROPERTY(QString generated_password READ generated_password NOTIFY generated_passwordChanged FINAL) + Q_PROPERTY(quint8 generator_state READ generator_state NOTIFY generator_stateChanged FINAL) + Q_PROPERTY(bool i_say_sexy_things_to_myself_while_im_dancing READ i_say_sexy_things_to_myself_while_im_dancing WRITE setI_say_sexy_things_to_myself_while_im_dancing NOTIFY i_say_sexy_things_to_myself_while_im_dancingChanged FINAL) + Q_PROPERTY(QString master_password READ master_password WRITE setMaster_password NOTIFY master_passwordChanged FINAL) + Q_PROPERTY(Profiles* profiles READ profiles NOTIFY profilesChanged FINAL) + Q_PROPERTY(Settings* settings READ settings NOTIFY settingsChanged FINAL) + Q_PROPERTY(QString url READ url WRITE setUrl NOTIFY urlChanged FINAL) + Q_PROPERTY(QString used_text READ used_text WRITE setUsed_text NOTIFY used_textChanged FINAL) + explicit PasswordMaker(bool owned, QObject *parent); +public: + explicit PasswordMaker(QObject *parent = nullptr); + ~PasswordMaker(); + QString generated_password() const; + quint8 generator_state() const; + bool i_say_sexy_things_to_myself_while_im_dancing() const; + void setI_say_sexy_things_to_myself_while_im_dancing(bool v); + QString master_password() const; + void setMaster_password(const QString& v); + const Profiles* profiles() const; + Profiles* profiles(); + const Settings* settings() const; + Settings* settings(); + QString url() const; + void setUrl(const QString& v); + QString used_text() const; + void setUsed_text(const QString& v); + Q_INVOKABLE void profile_changed() const; + Q_INVOKABLE bool store_settings() const; +Q_SIGNALS: + void generated_passwordChanged(); + void generator_stateChanged(); + void i_say_sexy_things_to_myself_while_im_dancingChanged(); + void master_passwordChanged(); + void profilesChanged(); + void settingsChanged(); + void urlChanged(); + void used_textChanged(); +}; + +class Profiles : public QAbstractItemModel +{ + Q_OBJECT + friend class PasswordMaker; +public: + class Private; +private: + const Private * m_d; + Private * const m_d_owned; + Q_PROPERTY(quint32 current_profile READ current_profile WRITE setCurrent_profile NOTIFY current_profileChanged FINAL) + Q_PROPERTY(QString current_profile_name READ current_profile_name NOTIFY current_profile_nameChanged FINAL) + explicit Profiles(bool owned, QObject *parent); +public: + explicit Profiles(QObject *parent = nullptr); + ~Profiles(); + quint32 current_profile() const; + void setCurrent_profile(quint32 v); + QString current_profile_name() const; + Q_INVOKABLE bool store() const; + + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex &index) const override; + bool hasChildren(const QModelIndex &parent = QModelIndex()) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + bool canFetchMore(const QModelIndex &parent) const override; + void fetchMore(const QModelIndex &parent) override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + void sort(int column, Qt::SortOrder order = Qt::AscendingOrder) override; + int role(const char* name) const; + QHash<int, QByteArray> roleNames() const override; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + bool setHeaderData(int section, Qt::Orientation orientation, const QVariant &value, int role = Qt::EditRole) override; + Q_INVOKABLE bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex()) override; + Q_INVOKABLE bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + Q_INVOKABLE QString characters(int row) const; + Q_INVOKABLE bool setCharacters(int row, const QString& value); + Q_INVOKABLE quint8 hash_algorithm(int row) const; + Q_INVOKABLE bool setHash_algorithm(int row, quint8 value); + Q_INVOKABLE quint8 leet_level(int row) const; + Q_INVOKABLE bool setLeet_level(int row, quint8 value); + Q_INVOKABLE QString modifier(int row) const; + Q_INVOKABLE bool setModifier(int row, const QString& value); + Q_INVOKABLE QString name(int row) const; + Q_INVOKABLE bool setName(int row, const QString& value); + Q_INVOKABLE quint32 password_length(int row) const; + Q_INVOKABLE bool setPassword_length(int row, quint32 value); + Q_INVOKABLE QString prefix(int row) const; + Q_INVOKABLE bool setPrefix(int row, const QString& value); + Q_INVOKABLE QString suffix(int row) const; + Q_INVOKABLE bool setSuffix(int row, const QString& value); + Q_INVOKABLE bool use_domain(int row) const; + Q_INVOKABLE bool setUse_domain(int row, bool value); + Q_INVOKABLE quint8 use_leet(int row) const; + Q_INVOKABLE bool setUse_leet(int row, quint8 value); + Q_INVOKABLE bool use_port_path(int row) const; + Q_INVOKABLE bool setUse_port_path(int row, bool value); + Q_INVOKABLE bool use_protocol(int row) const; + Q_INVOKABLE bool setUse_protocol(int row, bool value); + Q_INVOKABLE bool use_subdomains(int row) const; + Q_INVOKABLE bool setUse_subdomains(int row, bool value); + Q_INVOKABLE bool use_undefined_as_protocol_fallback(int row) const; + Q_INVOKABLE bool setUse_undefined_as_protocol_fallback(int row, bool value); + Q_INVOKABLE bool use_user_info(int row) const; + Q_INVOKABLE bool setUse_user_info(int row, bool value); + Q_INVOKABLE QString username(int row) const; + Q_INVOKABLE bool setUsername(int row, const QString& value); + +Q_SIGNALS: + // new data is ready to be made available to the model with fetchMore() + // the passed QVariant contains a quintptr which is the internal id. + // This is the same index that the fetch_more function typically expects on the Rust side. + // Previously this Signal sent a QModelIndex, but that was changed due to threading issues. + // If you need to create a QModelIndex from the internal id, you can expose the profiles_row() method + // on the underlying Rust object as a function and use it. Just beware that this Signal is + // typically sent from arbitrary threads, so compute the index at the Slot side. + // While speaking of threads: Make sure the internal ID of your model entries is stable. + // If it can change, it's up to you to ensure that it does not do so between sending this + // signal and receiving the corresponding fetchMore() call on the other side of the Connection. + void newDataReady(const QVariant &parent) const; +private: + QHash<QPair<int,Qt::ItemDataRole>, QVariant> m_headerData; + void initHeaderData(); + void updatePersistentIndexes(); +Q_SIGNALS: + void current_profileChanged(); + void current_profile_nameChanged(); +}; + +class Settings : public QObject +{ + Q_OBJECT + friend class PasswordMaker; +public: + class Private; +private: + const Private * m_d; + Private * const m_d_owned; + Q_PROPERTY(QVariant clear_generated_password_seconds READ clear_generated_password_seconds WRITE setClear_generated_password_seconds NOTIFY clear_generated_password_secondsChanged FINAL) + Q_PROPERTY(QVariant clear_master_password_seconds READ clear_master_password_seconds WRITE setClear_master_password_seconds NOTIFY clear_master_password_secondsChanged FINAL) + Q_PROPERTY(bool hide_generated_password READ hide_generated_password WRITE setHide_generated_password NOTIFY hide_generated_passwordChanged FINAL) + explicit Settings(bool owned, QObject *parent); +public: + explicit Settings(QObject *parent = nullptr); + ~Settings(); + QVariant clear_generated_password_seconds() const; + void setClear_generated_password_seconds(const QVariant& v); + QVariant clear_master_password_seconds() const; + void setClear_master_password_seconds(const QVariant& v); + bool hide_generated_password() const; + void setHide_generated_password(bool v); +Q_SIGNALS: + void clear_generated_password_secondsChanged(); + void clear_master_password_secondsChanged(); + void hide_generated_passwordChanged(); +}; +#endif // BINDINGS_H diff --git a/src/GraphemeCountValidator.cpp b/src/GraphemeCountValidator.cpp new file mode 100644 index 0000000..9fc6436 --- /dev/null +++ b/src/GraphemeCountValidator.cpp @@ -0,0 +1,42 @@ +#include "GraphemeCountValidator.h" +#include <QTextBoundaryFinder> + +GraphemeCountValidator::GraphemeCountValidator(QObject *parent) + : GraphemeCountValidator(0, parent) +{} + +GraphemeCountValidator::GraphemeCountValidator(uint min_count, QObject *parent) + : QValidator(parent) + , minCount(min_count) +{} + +uint GraphemeCountValidator::minGraphemeCount() const +{ + return minCount; +} + +void GraphemeCountValidator::setMinGraphemeCount(uint c) +{ + if(minCount != c) + { + minCount = c; + emit minGraphemeCountChanged(minCount); + emit changed(); + } +} + +QValidator::State GraphemeCountValidator::validate(QString & text, int&) const +{ + //One could write Rust FFI bindings and do this in Rust. + //But that would be way more work for no gain. + QTextBoundaryFinder boundsFinder{ + QTextBoundaryFinder::BoundaryType::Grapheme, + text + }; + int requiredBoundary = 0; + for(uint i = 0; i < minCount; ++i) + requiredBoundary = boundsFinder.toNextBoundary(); + return requiredBoundary >= 0 + ? QValidator::State::Acceptable + : QValidator::State::Intermediate; +} diff --git a/src/GraphemeCountValidator.h b/src/GraphemeCountValidator.h new file mode 100644 index 0000000..e2a5dc7 --- /dev/null +++ b/src/GraphemeCountValidator.h @@ -0,0 +1,26 @@ +#ifndef GRAPHEMECOUNTVALIDATOR_H +#define GRAPHEMECOUNTVALIDATOR_H + +#include <QValidator> + +class Q_GUI_EXPORT GraphemeCountValidator : public QValidator +{ + Q_OBJECT + Q_PROPERTY(uint minGraphemeCount READ minGraphemeCount WRITE setMinGraphemeCount NOTIFY minGraphemeCountChanged) +public: + explicit GraphemeCountValidator(QObject *parent = nullptr); + explicit GraphemeCountValidator(uint min_count, QObject *parent = nullptr); + virtual QValidator::State validate(QString &, int &) const override; + + uint minGraphemeCount() const; + void setMinGraphemeCount(uint c); + + +Q_SIGNALS: + void minGraphemeCountChanged(uint minGraphemeCount); + +private: + uint minCount; +}; + +#endif // GRAPHEMECOUNTVALIDATOR_H diff --git a/src/PassFish.cpp b/src/PassFish.cpp new file mode 100644 index 0000000..80eef2a --- /dev/null +++ b/src/PassFish.cpp @@ -0,0 +1,28 @@ +#include <QtQuick> +#include <sailfishapp.h> +#include <stdio.h> +#include "Bindings.h" +#include "GraphemeCountValidator.h" + +Q_DECL_EXPORT int main(int argc, char *argv[]) +{ + QScopedPointer<QGuiApplication> app{SailfishApp::application(argc,argv)}; + QScopedPointer<QQuickView> view{SailfishApp::createView()}; + + qmlRegisterType<GraphemeCountValidator>("PWM", 1, 0, "GraphemeCountValidator"); + + PasswordMaker maker; + view->rootContext()->setContextProperty("passwordmaker",&maker); + QObject::connect(&maker, + &PasswordMaker::i_say_sexy_things_to_myself_while_im_dancingChanged, + &maker, + [&]() {maker.setI_say_sexy_things_to_myself_while_im_dancing(true);}, + (Qt::ConnectionType)(Qt::AutoConnection | Qt::UniqueConnection) + ); + + view->setSource(SailfishApp::pathTo(QString("qml/PassFish.qml"))); + view->show(); + + return app->exec(); +} + diff --git a/src/pwm_qhash.cpp b/src/pwm_qhash.cpp new file mode 100644 index 0000000..f9528d4 --- /dev/null +++ b/src/pwm_qhash.cpp @@ -0,0 +1,22 @@ +#include <QCryptographicHash> + +extern "C" { + /** + * Forwards the call to QCryptographicHash::hash() and copies the output into the provided buffer. + * Returns zero on error (unsupported hash type or insufficient output_capacity). Number of written output bytes otherwise. + */ + Q_DECL_EXPORT size_t pwm_qhash(size_t algorithm, const unsigned char * input, size_t input_length, unsigned char * output, size_t output_capacity) + { + if(algorithm > 6) + return 0; //failed. + const auto algo{static_cast<QCryptographicHash::Algorithm>(algorithm)}; + QCryptographicHash hasher{algo}; + hasher.addData(reinterpret_cast<const char *>(input), input_length); + const auto hash{hasher.result()}; + const auto count_as_size{static_cast<size_t>(hash.length())}; + if(count_as_size > output_capacity) + return 0; + memcpy(output,hash.constData(),count_as_size); + return count_as_size; + } +} diff --git a/translations/Passwordsailor-de.ts b/translations/Passwordsailor-de.ts new file mode 100644 index 0000000..8f39883 --- /dev/null +++ b/translations/Passwordsailor-de.ts @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="utf-8"?> +<!DOCTYPE TS> +<TS version="2.1"> +<context> + <name>CoverPage</name> + <message> + <source>My Cover</source> + <translation>Mein Cover</translation> + </message> +</context> +<context> + <name>FirstPage</name> + <message> + <source>Show Page 2</source> + <translation>Zur Seite 2</translation> + </message> + <message> + <source>UI Template</source> + <translation>UI-Vorlage</translation> + </message> + <message> + <source>Hello Sailors</source> + <translation>Hallo Matrosen</translation> + </message> +</context> +<context> + <name>SecondPage</name> + <message> + <source>Nested Page</source> + <translation>Unterseite</translation> + </message> + <message> + <source>Item</source> + <translation>Element</translation> + </message> +</context> +</TS> diff --git a/translations/Passwordsailor.ts b/translations/Passwordsailor.ts new file mode 100644 index 0000000..7fc118e --- /dev/null +++ b/translations/Passwordsailor.ts @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="utf-8"?> +<!DOCTYPE TS> +<TS version="2.1"> +<context> + <name>CoverPage</name> + <message> + <source>My Cover</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>FirstPage</name> + <message> + <source>Show Page 2</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>UI Template</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Hello Sailors</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>SecondPage</name> + <message> + <source>Nested Page</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Item</source> + <translation type="unfinished"></translation> + </message> +</context> +</TS> |
