Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ rust-version = "1.95.0"
tauri-build = { version = "2.5.6", features = [] }

[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "2.10.3", features = [] }
open = "5.3.3"
rand = "0.10.1"
Expand Down
141 changes: 93 additions & 48 deletions src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,52 @@
)]

use rand::RngExt;
use std::fs;
use std::{collections::HashSet, fmt::Display, fs};
use unicode_segmentation::UnicodeSegmentation;

fn main() {
#[cfg(target_os = "linux")]
type CommandResult<T> = Result<T, String>;

#[cfg(target_os = "linux")]
fn configure_linux_environment() {
// SAFETY: This runs during application startup before any threads are spawned
// and only sets a fixed process-wide environment variable needed on Linux.
unsafe {
std::env::set_var("__NV_DISABLE_EXPLICIT_SYNC", "1");
}
}

fn map_command_result<T, E>(result: Result<T, E>) -> CommandResult<T>
where
E: Display,
{
result.map_err(|error| error.to_string())
}

fn as_platform_size(value: u64, field_name: &str) -> CommandResult<usize> {
usize::try_from(value).map_err(|_| format!("{field_name} is too large for this platform."))
}

fn count_unique_passwords(min_length: usize, max_length: usize, grapheme_count: usize) -> usize {
let mut total = 0usize;

for length in min_length..=max_length {
let combinations = u32::try_from(length)
.ok()
.and_then(|value| grapheme_count.checked_pow(value))
.unwrap_or(usize::MAX);

total = total.saturating_add(combinations);
if total == usize::MAX {
break;
}
}

total
}

fn main() {
#[cfg(target_os = "linux")]
configure_linux_environment();

tauri::Builder::default()
.plugin(tauri_plugin_os::init())
Expand All @@ -34,76 +72,83 @@ fn exit_app() {
}

#[tauri::command]
fn open_website(website: &str) -> Result<String, String> {
match open::that(website) {
Ok(_) => Ok(String::from("Success")),
Err(e) => Err(e.to_string()),
}
fn open_website(website: &str) -> CommandResult<()> {
map_command_result(open::that(website))
}

#[tauri::command]
fn save_string_to_disk(content: &str, path: &str) -> Result<String, String> {
match fs::write(path, content) {
Ok(_) => Ok(String::from("Success")),
Err(e) => Err(e.to_string()),
}
fn save_string_to_disk(content: &str, path: &str) -> CommandResult<()> {
map_command_result(fs::write(path, content))
}

#[tauri::command]
async fn read_string_from_file(path: &str) -> Result<String, String> {
match fs::read_to_string(path) {
Ok(s) => Ok(s),
Err(e) => Err(e.to_string()),
}
fn read_string_from_file(path: &str) -> CommandResult<String> {
map_command_result(fs::read_to_string(path))
}

#[tauri::command]
async fn generate_passwords(
fn generate_passwords(
min_length: u64,
max_length: u64,
character_set: &str,
include_symbols: &str,
amount: u64,
allow_duplicates: bool,
) -> Result<Vec<String>, String> {
let mut password_list: Vec<String> = Vec::new();
let mut max_count: f64 = 0.0;
) -> CommandResult<Vec<String>> {
let min_length = as_platform_size(min_length, "Minimum length")?;
let max_length = as_platform_size(max_length, "Maximum length")?;
let requested_amount = as_platform_size(amount, "Password amount")?;

let mut total_character_set = String::from(character_set);
total_character_set.push_str(include_symbols);
if min_length > max_length {
return Err("Minimum length cannot be greater than maximum length.".into());
}

if requested_amount == 0 {
return Ok(Vec::new());
}

let graphemes = total_character_set.graphemes(true);
let char_count = graphemes.clone().count();
let total_character_set = format!("{character_set}{include_symbols}");
let graphemes = total_character_set.graphemes(true).collect::<Vec<&str>>();
let grapheme_count = graphemes.len();

if !allow_duplicates {
let mut current = min_length;
while current <= max_length {
max_count += (char_count as f64).powf(current as f64);
current += 1;
}
if grapheme_count == 0 {
return Ok(Vec::new());
}

let target_amount = if allow_duplicates {
requested_amount
} else {
requested_amount.min(count_unique_passwords(
min_length,
max_length,
grapheme_count,
))
};

if target_amount == 0 {
return Ok(Vec::new());
}

let mut password_list = Vec::with_capacity(target_amount);
let mut seen_passwords = (!allow_duplicates).then(|| HashSet::with_capacity(target_amount));
Comment thread
CodeDead marked this conversation as resolved.
let mut rng = rand::rng();
let chars = graphemes.collect::<Vec<&str>>();
for _n in 0..amount {
let mut can_continue = false;
while !can_continue {
let mut password = String::from("");
let length = rng.random_range(min_length..(max_length + 1));
for _j in 0..length {
let index = rng.random_range(0..char_count);
password.push_str(chars.clone()[index]);
}

if allow_duplicates || !password_list.contains(&password) {
password_list.push(password);
can_continue = true;
}
while password_list.len() < target_amount {
let length = rng.random_range(min_length..=max_length);
let mut password = String::new();

for _ in 0..length {
let index = rng.random_range(0..grapheme_count);
password.push_str(graphemes[index]);
}

if !can_continue && !allow_duplicates && password_list.len() as f64 == max_count {
return Ok(password_list);
if let Some(seen_passwords) = seen_passwords.as_mut() {
if !seen_passwords.insert(password.clone()) {
continue;
}
}

password_list.push(password);
}

Ok(password_list)
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
}
}
},
"productName": "advanced-passgen",
"productName": "Advanced PassGen",
"mainBinaryName": "advanced-passgen",
"version": "2.5.2",
"identifier": "com.codedead.advancedpassgen",
Expand Down
100 changes: 49 additions & 51 deletions src/components/PasswordTips/index.jsx
Original file line number Diff line number Diff line change
@@ -1,87 +1,85 @@
import React, { useContext, useEffect, useRef, useState } from 'react';
import React, {
useCallback,
useContext,
useEffect,
useRef,
useState,
} from 'react';
import CloseIcon from '@mui/icons-material/Close';
import Alert from '@mui/material/Alert';
import AlertTitle from '@mui/material/AlertTitle';
import Collapse from '@mui/material/Collapse';
import IconButton from '@mui/material/IconButton';
import { MainContext } from '../../contexts/MainContextProvider';

const getRandomTipIndex = (length) =>
length > 0 ? Math.floor(Math.random() * length) : 0;

const PasswordTips = () => {
const [state] = useContext(MainContext);

const { languageIndex, tips } = state;
const language = state.languages[languageIndex];

const [tipsOpen, setTipsOpen] = useState(tips);
const intervalId = useRef();
const [currentTip, setCurrentTip] = useState(
language.passwordTips[
// eslint-disable-next-line react-hooks/purity
Math.floor(Math.random() * language.passwordTips.length)
],
const intervalId = useRef(null);
const [tipIndex, setTipIndex] = useState(() =>
getRandomTipIndex(language.passwordTips.length),
);
Comment thread
CodeDead marked this conversation as resolved.
const currentTip =
language.passwordTips[tipIndex % language.passwordTips.length] ?? '';

/**
* Generate a new tipper
* Clear the active tip timer
*/
const generateNewTipper = () => {
const clearTipInterval = useCallback(() => {
if (intervalId.current !== null) {
clearInterval(intervalId.current);
intervalId.current = null;
}
}, []);

intervalId.current = setInterval(() => {
setCurrentTip(
language.passwordTips[
Math.floor(Math.random() * language.passwordTips.length)
],
);
}, 30000);
};
/**
* Start rotating the current tip
* @param passwordTips The available tips for the current language
*/
const startTipRotation = useCallback(
(passwordTips) => {
clearTipInterval();

useEffect(() => {
if (tips) {
generateNewTipper();
}
if (!passwordTips || passwordTips.length === 0) {
return;
}

return () =>
intervalId !== null ? clearInterval(intervalId.current) : null;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
intervalId.current = setInterval(() => {
setTipIndex(getRandomTipIndex(passwordTips.length));
}, 30000);
},
[clearTipInterval],
);

useEffect(() => {
if (tips) {
// eslint-disable-next-line react-hooks/set-state-in-effect
setCurrentTip(
language.passwordTips[
Math.floor(Math.random() * language.passwordTips.length)
],
);
if (!tips) {
clearTipInterval();
return clearTipInterval;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [language]);

useEffect(() => {
if (tips) {
generateNewTipper();
} else {
clearInterval(intervalId.current);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tips]);
startTipRotation(language.passwordTips);

return clearTipInterval;
}, [clearTipInterval, language.passwordTips, startTipRotation, tips]);

const closeTips = () => {
setTipsOpen(false);
clearTipInterval();
};

return (
<Collapse in={tipsOpen}>
<Collapse in={tips && tipsOpen}>
<Alert
severity="info"
action={
<IconButton
aria-label="close"
color="inherit"
onClick={() => {
setTipsOpen(false);
clearInterval(intervalId.current);
}}
>
<IconButton aria-label="close" color="inherit" onClick={closeTips}>
<CloseIcon fontSize="inherit" />
</IconButton>
}
Expand Down
26 changes: 25 additions & 1 deletion src/reducers/MainReducer/Actions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,30 @@ import {
SET_UPDATE,
} from './actionTypes';

const normalizeErrorMessage = (error) => {
if (error === null || error === undefined) {
return null;
}

if (typeof error === 'string') {
return error;
}

if (error instanceof Error) {
return error.message;
}

if (
typeof error === 'object' &&
'message' in error &&
typeof error.message === 'string'
) {
return error.message;
}

return String(error);
};

export const setLanguageIndex = (index) => ({
type: SET_LANGUAGE_INDEX,
payload: index,
Expand Down Expand Up @@ -62,7 +86,7 @@ export const setUpdate = (update) => ({

export const setError = (error) => ({
type: SET_ERROR,
payload: error,
payload: normalizeErrorMessage(error),
});

export const setLanguageSelector = (value) => ({
Expand Down
Loading
Loading