1 The Problem
We want a tool that creates a strong password: a random mix of letters, numbers, and symbols, of a length the user chooses. It teaches building a string from random choices — and an important lesson about which randomness is safe for security.
2 How to Think About It
A password generator is really two decisions: which characters are allowed, and how many of them to pick at random.
length times. → 3. Collect the picks into a String.
3 The Build — explained part by part
Here is the complete generator. Read the callout below before trusting this for anything real — it is deliberately honest about a sandbox limitation rather than pretending otherwise.
use std::collections::hash_map::RandomState;
use std::env;
use std::hash::{BuildHasher, Hasher};
const LOWER: &[u8] = b"abcdefghijklmnopqrstuvwxyz";
const UPPER: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const DIGITS: &[u8] = b"0123456789";
const SYMBOLS: &[u8] = b"!@#$%^&*()-_=+";
/// A NON-CRYPTOGRAPHIC source of randomness, used here only because this
/// build environment cannot fetch external crates. `RandomState` reseeds
/// itself from the OS's random source on every call and is enough to make a
/// demo password generator behave differently each run, but it is not the
/// tool for a real one. Go's version of this project used `crypto/rand`
/// specifically because password generation is security-sensitive; the
/// direct Rust equivalent is the `rand` crate's `rngs::OsRng` (`cargo add
/// rand`, then `let mut rng = rand::rngs::OsRng;`) — a real project should
/// use that, not this.
fn random_index(len: usize) -> usize {
let n = RandomState::new().build_hasher().finish();
(n % len as u64) as usize
}
/// Builds the character pool to draw from, based on which categories are
/// requested. Returns `None` if every category was turned off — including
/// lowercase, which is why it is a flag here too rather than always-on.
fn build_pool(use_lower: bool, use_upper: bool, use_digits: bool, use_symbols: bool) -> Option<Vec<u8>> {
let mut pool = Vec::new();
if use_lower {
pool.extend_from_slice(LOWER);
}
if use_upper {
pool.extend_from_slice(UPPER);
}
if use_digits {
pool.extend_from_slice(DIGITS);
}
if use_symbols {
pool.extend_from_slice(SYMBOLS);
}
if pool.is_empty() {
None
} else {
Some(pool)
}
}
fn generate(length: usize, pool: &[u8]) -> String {
(0..length)
.map(|_| pool[random_index(pool.len())] as char)
.collect()
}
fn main() {
let length: usize = env::args()
.nth(1)
.and_then(|s| s.parse().ok())
.unwrap_or(16);
let pool = match build_pool(true, true, true, true) {
Some(p) => p,
None => {
eprintln!("No character categories selected.");
return;
}
};
println!("{}", generate(length, &pool));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generates_a_password_of_the_requested_length() {
let pool = build_pool(true, true, true, true).unwrap();
let pw = generate(20, &pool);
assert_eq!(pw.chars().count(), 20);
}
#[test]
fn only_uses_characters_from_the_pool() {
let pool = build_pool(true, false, true, false).unwrap(); // lower + digits only
let pw = generate(200, &pool);
assert!(pw.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit()));
}
#[test]
fn empty_pool_is_rejected_when_every_category_is_off() {
assert!(build_pool(false, false, false, false).is_none());
}
#[test]
fn random_index_stays_in_bounds() {
for _ in 0..500 {
let i = random_index(10);
assert!(i < 10);
}
}
}
rustup) is installed.b"..."), a slice of raw ASCII bytes rather than a &str, because we want to pick individual bytes by index and cast them back to char.build_pool(use_lower, use_upper, use_digits, use_symbols) -> Option<Vec<u8>> — returns
None if every category is turned off, which forces main to handle the “no characters to choose from” case explicitly instead of generating an empty or garbage password.(0..length).map(|_| pool[random_index(pool.len())] as char).collect() — a range iterator run
length times, each iteration picking one random byte from the pool and casting it to char; .collect() gathers the resulting chars straight into a String because Rust infers the target type from the function’s return type.
RandomState-derived randomness as cryptographically secure — it is a convenient stand-in for this sandbox, not a replacement for a real CSPRNG.rand crate’s OsRng, which wraps the operating system’s cryptographic random source — exactly the distinction Go’s version of this project draws between crypto/rand and math/rand.cargo test caught it immediately: a test asserting an all-categories-off pool is empty failed, because lowercase was still being added.None.4 Test & Prove Each Part
We test that the pool logic and the generated output actually obey the rules they claim to.
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generates_a_password_of_the_requested_length() {
let pool = build_pool(true, true, true, true).unwrap();
let pw = generate(20, &pool);
assert_eq!(pw.chars().count(), 20);
}
#[test]
fn only_uses_characters_from_the_pool() {
let pool = build_pool(true, false, true, false).unwrap(); // lower + digits only
let pw = generate(200, &pool);
assert!(pw.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit()));
}
#[test]
fn empty_pool_is_rejected_when_every_category_is_off() {
assert!(build_pool(false, false, false, false).is_none());
}
#[test]
fn random_index_stays_in_bounds() {
for _ in 0..500 {
let i = random_index(10);
assert!(i < 10);
}
}
}
Run with cargo test. This project’s own history is worth reading here: the first version of build_pool always added lowercase letters regardless of its flags, so the “every category off” test failed loudly instead of the bug slipping through — exactly what tests are for.
5 The Interface
What it expects
16 (as a command-line argument; defaults to 16)What it returns
L9$Ejr%GjxNx!80C6 Run It & Automate It
Save the code as src/main.rs inside a Cargo project's src/ folder and run it with cargo run — Cargo compiles and executes in one step while you are experimenting, then cargo build --release gives you an optimized binary once you are done.
cargo run -- 20Prints one password of the requested length to stdout.
A CI tool like Jenkins runs cargo test automatically whenever the code changes — every line below has a plain explanation.
$ cargo run -- 16
L9$Ejr%GjxNx!80Cmain to pass all four flags as false. Restore at least one true flag in the build_pool call.generate counts bytes from an ASCII pool, so this should not happen with the pools defined here — if you add non-ASCII symbols to a pool, cast carefully, since a multi-byte character would break the simple byte-to-char cast used here.// Jenkinsfile — runs the tests automatically every time the code changes.
pipeline {
agent any // run on any available machine
stages {
stage('Get the code') {
steps { checkout scm } // download the latest code
}
stage('Set up Rust') {
steps {
sh 'rustc --version' // confirm Rust is installed
sh 'cargo build' // compile, downloading any crates
}
}
stage('Run the tests') {
steps {
sh 'cargo clippy -- -D warnings' // catch obvious mistakes before running
sh 'cargo test' // run every test, show each result
}
}
}
post {
success { echo 'All tests passed.' }
failure { echo 'A test failed — look above.' }
}
}
- Use the real
randcrate. If you have network access,cargo add randand generate withrand::rngs::OsRng. (Teaches: the actual production-grade approach.) - Guarantee category coverage. Force at least one uppercase letter, digit, and symbol into every password. (Teaches: mixing guaranteed picks with random ones.)
- Add a strength estimate. Report roughly how many bits of entropy the password has, based on pool size and length. (Teaches: a bit of information theory.)