diff --git a/Cargo.lock b/Cargo.lock
index 6ccc376326370915e1d650a462a496c651947e10..e5907165271c519889e8fd98b613c30c34cb671e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1,5 +1,14 @@
 # This file is automatically @generated by Cargo.
 # It is not intended for manual editing.
+[[package]]
+name = "aho-corasick"
+version = "0.7.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5"
+dependencies = [
+ "memchr",
+]
+
 [[package]]
 name = "ansi_term"
 version = "0.11.0"
@@ -91,18 +100,6 @@ version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "476e9cd489f9e121e02ffa6014a8ef220ecb15c05ed23fc34cca13925dc283fb"
 
-[[package]]
-name = "bstr"
-version = "0.2.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "473fc6b38233f9af7baa94fb5852dca389e3d95b8e21c8e3719301462c5d9faf"
-dependencies = [
- "lazy_static",
- "memchr",
- "regex-automata",
- "serde",
-]
-
 [[package]]
 name = "bumpalo"
 version = "3.4.0"
@@ -237,28 +234,6 @@ version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "da24927b5b899890bcb29205436c957b7892ec3a3fbffce81d710b9611e77778"
 
-[[package]]
-name = "csv"
-version = "1.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f9d58633299b24b515ac72a3f869f8b91306a3cec616a602843a383acd6f9e97"
-dependencies = [
- "bstr",
- "csv-core",
- "itoa",
- "ryu",
- "serde",
-]
-
-[[package]]
-name = "csv-core"
-version = "0.1.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90"
-dependencies = [
- "memchr",
-]
-
 [[package]]
 name = "digest"
 version = "0.9.0"
@@ -304,10 +279,10 @@ name = "g1force"
 version = "0.1.0"
 dependencies = [
  "bruteforce",
- "csv",
  "dup-crypto",
  "indicatif",
  "rayon",
+ "regex",
  "structopt",
 ]
 
@@ -364,12 +339,6 @@ dependencies = [
  "regex",
 ]
 
-[[package]]
-name = "itoa"
-version = "0.4.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
-
 [[package]]
 name = "js-sys"
 version = "0.3.46"
@@ -516,16 +485,10 @@ version = "1.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d9251239e129e16308e70d853559389de218ac275b515068abc96829d05b948a"
 dependencies = [
+ "aho-corasick",
+ "memchr",
  "regex-syntax",
-]
-
-[[package]]
-name = "regex-automata"
-version = "0.1.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ae1ded71d66a4a97f5e961fd0cb25a5f366a42a41570d16a763a69c092c26ae4"
-dependencies = [
- "byteorder",
+ "thread_local",
 ]
 
 [[package]]
@@ -549,12 +512,6 @@ dependencies = [
  "winapi",
 ]
 
-[[package]]
-name = "ryu"
-version = "1.0.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
-
 [[package]]
 name = "scopeguard"
 version = "1.1.0"
@@ -685,6 +642,15 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "thread_local"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb9bc092d0d51e76b2b19d9d85534ffc9ec2db959a2523cdae0697e2972cd447"
+dependencies = [
+ "lazy_static",
+]
+
 [[package]]
 name = "typenum"
 version = "1.12.0"
diff --git a/Cargo.toml b/Cargo.toml
index ac926510b71a826aae2e6581920f160514cdf32a..9d35faa2380a81d47bc0c84e6be1f9d77c9646bb 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,8 +7,8 @@ license = "AGPL3"
 
 [dependencies]
 bruteforce = "0.2.0"
-csv = "1.1.5"
 dup-crypto = { version = "0.38.0", default-features = false, features = ["scrypt"] }
 indicatif = "0.15.0"
 rayon = "1.5.0"
+regex = "1.4.3"
 structopt = "0.3.21"
diff --git a/README.md b/README.md
index 1b4442c11b079bacd0134fcf943ae1e30de2731b..c65ca2581b91aaeb9107661d3357c88289c26dcf 100644
--- a/README.md
+++ b/README.md
@@ -4,6 +4,8 @@ Scrypt bruteforce for Duniter currencies
 
 ## Build
 
+You need to switch to Rust Nightly first.
+
     cargo build --release
 
 ## Use
@@ -31,4 +33,4 @@ Dictionary attack should be implemented.
 
 ## License
 
-GNU AGPL v3, CopyLeft 2020 Pascal Engélibert
+GNU AGPL v3, CopyLeft 2020-2021 Pascal Engélibert
diff --git a/src/main.rs b/src/main.rs
index f5fa8df1dd712ca72b58b03f22d7d803eac6c208..b8f05b769174f3bcca21ed347dc14c958cb2ce2b 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,3 +1,8 @@
+#![feature(bool_to_option)]
+#![feature(iter_map_while)]
+#![feature(maybe_uninit_ref)]
+#![feature(str_split_once)]
+
 use bruteforce::{charset::Charset, BruteForce};
 use dup_crypto::{
 	bases::b58::ToBase58,
@@ -8,7 +13,13 @@ use dup_crypto::{
 };
 use indicatif::{ProgressBar, ProgressStyle};
 use rayon::prelude::*;
-use std::{collections::HashMap, io::stdin, sync::Mutex};
+use regex::Regex;
+use std::{
+	collections::HashMap,
+	io::{stdin, BufRead, BufReader},
+	mem::MaybeUninit,
+	sync::RwLock,
+};
 
 use structopt::StructOpt;
 
@@ -19,7 +30,7 @@ const CHARSET: Charset = Charset::new(&[
 	'5', '6', '7', '8', '9', '_', '.', '-', '!', '@', '*', '$', '?', '&', '%', ' ',
 ]);
 
-struct PubkeyData {
+struct PubkeyMetadata {
 	amount: u32,
 	member: bool,
 }
@@ -27,155 +38,223 @@ struct PubkeyData {
 #[derive(Clone, Debug, StructOpt)]
 #[structopt(name = "g1force")]
 pub struct MainOpt {
+	/// Use metadata in CSV (amount & member)
+	#[structopt(short, long)]
+	metadata: bool,
+
 	/// Number of iterations
 	#[structopt(short, long)]
 	ntests: usize,
 
+	/// Use regex (Perl-style)
 	#[structopt(short, long)]
-	wanted_pubkey: Option<String>,
+	regex: bool,
+
+	/// Wanted pubkey or regex. If empty, reads stdin.
+	#[structopt(short, long)]
+	pubkey: Option<String>,
 }
 
-fn main() {
-	let opt = MainOpt::from_args();
+enum InputData {
+	Pubkey(PublicKey),
+	PubkeyList(Vec<PublicKey>),
+	PubkeyListWithMetadata(HashMap<PublicKey, PubkeyMetadata>),
+	Regex(Regex),
+}
 
-	if let Some(ref wanted_pubkey) = opt.wanted_pubkey {
-		let wanted_pubkey = PublicKey::from_base58(&wanted_pubkey).expect("Invalid pubkey");
-
-		let generator = KeyPairFromSaltedPasswordGenerator::with_default_parameters();
-
-		let progress_bar = ProgressBar::new((opt.ntests as u64).pow(2));
-		progress_bar.set_style(
-			ProgressStyle::default_bar().template(
-				"{elapsed_precise}  {eta}  {per_sec}  {pos}/{len}  {percent}% {wide_bar}",
-			),
-		);
-
-		let mut i = 0usize;
-		let mut bf1 = BruteForce::new(CHARSET);
-		let mut buf = Vec::<String>::new();
-		progress_bar.tick();
-		loop {
-			while buf.len() < 16 && i < opt.ntests {
-				if let Some(s1) = bf1.next() {
-					buf.push(s1);
-					i += 1;
-				} else {
-					break;
-				}
+impl InputData {
+	fn new(opt: &MainOpt) -> Self {
+		if let Some(ref pubkey) = opt.pubkey {
+			if opt.regex {
+				Self::Regex(Regex::new(pubkey).expect("Invalid regex"))
+			} else {
+				Self::Pubkey(PublicKey::from_base58(pubkey).expect("Invalid pubkey"))
 			}
-			if buf.is_empty() {
-				break;
+		} else if opt.metadata {
+			let mut pubkeys = HashMap::<PublicKey, PubkeyMetadata>::new();
+
+			for (i, raw_line) in BufReader::new(stdin()).lines().enumerate() {
+				let raw_line = raw_line.expect("Error reading CSV");
+				let mut rows = raw_line.split(&[' ', '\t', ',', ';'][..]);
+				let pubkey = PublicKey::from_base58(
+					rows.next().expect(&format!("Missing pubkey line {}", i)),
+				)
+				.expect(&format!("Invalid pubkey line {}", i));
+				pubkeys.insert(
+					pubkey,
+					PubkeyMetadata {
+						amount: rows
+							.next()
+							.expect(&format!("Missing amount line {}", i))
+							.parse()
+							.expect(&format!("Invalid amount line {}", i)),
+						member: rows.next().expect(&format!("Missing member line {}", i)) == "1",
+					},
+				);
 			}
-			buf.par_iter().for_each(|s1| {
-				let mut j = 0usize;
-				let bf2 = BruteForce::new(CHARSET);
-				for s2 in bf2 {
-					let pubkey = generator
-						.generate(SaltedPassword::new(s1.clone(), s2.clone()))
-						.public_key();
-					if wanted_pubkey == pubkey {
-						eprintln!("Found!");
-						println!("{}\t{}", s1, s2);
-						std::process::exit(0);
-					}
-					if j >= opt.ntests {
-						break;
-					}
-					j += 1;
-				}
-				progress_bar.inc(opt.ntests as u64);
-			});
-			buf.clear();
+			Self::PubkeyListWithMetadata(pubkeys)
+		} else {
+			let mut pubkeys = Vec::<PublicKey>::new();
+
+			for (i, raw_line) in BufReader::new(stdin()).lines().enumerate() {
+				let raw_line = raw_line.expect("Error reading CSV");
+				let pubkey = PublicKey::from_base58(
+					raw_line
+						.split_once(&[' ', '\t', ',', ';'][..])
+						.expect(&format!("Missing pubkey line {}", i))
+						.0,
+				)
+				.expect(&format!("Invalid pubkey line {}", i));
+				pubkeys.push(pubkey);
+			}
+			Self::PubkeyList(pubkeys)
 		}
-		eprintln!("Not found...\n");
-	} else {
-		eprintln!("Importing data...");
-
-		let mut pubkeys = HashMap::<PublicKey, PubkeyData>::new();
-
-		let mut reader = csv::Reader::from_reader(stdin());
-		for raw_line in reader.records().filter_map(|x| x.ok()) {
-			let mut rows = raw_line.as_slice().split('\t');
-			let pubkey = PublicKey::from_base58(rows.next().unwrap()).unwrap();
-			pubkeys.insert(
-				pubkey,
-				PubkeyData {
-					amount: rows.next().unwrap().parse().unwrap(),
-					member: rows.next().unwrap() == "1",
-				},
-			);
+	}
+}
+
+struct Tester<'a> {
+	pubkey: MaybeUninit<&'a PublicKey>,
+	pubkey_list: MaybeUninit<&'a Vec<PublicKey>>,
+	pubkey_list_with_metadata: MaybeUninit<&'a HashMap<PublicKey, PubkeyMetadata>>,
+	regex: MaybeUninit<&'a Regex>,
+
+	/// Returns (match, stop)
+	test: fn(&Self, &PublicKey) -> (bool, bool),
+}
+
+impl<'a> Tester<'a> {
+	fn new(data: &'a InputData) -> Self {
+		match data {
+			InputData::Pubkey(pubkey) => Self {
+				pubkey: MaybeUninit::new(pubkey),
+				pubkey_list: MaybeUninit::uninit(),
+				pubkey_list_with_metadata: MaybeUninit::uninit(),
+				regex: MaybeUninit::uninit(),
+				test: Self::test_pubkey,
+			},
+			InputData::PubkeyList(pubkey_list) => Self {
+				pubkey: MaybeUninit::uninit(),
+				pubkey_list: MaybeUninit::new(pubkey_list),
+				pubkey_list_with_metadata: MaybeUninit::uninit(),
+				regex: MaybeUninit::uninit(),
+				test: Self::test_pubkey_list,
+			},
+			InputData::PubkeyListWithMetadata(pubkey_list_with_metadata) => Self {
+				pubkey: MaybeUninit::uninit(),
+				pubkey_list: MaybeUninit::uninit(),
+				pubkey_list_with_metadata: MaybeUninit::new(pubkey_list_with_metadata),
+				regex: MaybeUninit::uninit(),
+				test: Self::test_pubkey_list_with_metadata,
+			},
+			InputData::Regex(regex) => Self {
+				pubkey: MaybeUninit::uninit(),
+				pubkey_list: MaybeUninit::uninit(),
+				pubkey_list_with_metadata: MaybeUninit::uninit(),
+				regex: MaybeUninit::new(regex),
+				test: Self::test_regex,
+			},
 		}
+	}
 
-		let found = Mutex::new(Vec::<(PublicKey, String, String)>::new());
-
-		let generator = KeyPairFromSaltedPasswordGenerator::with_default_parameters();
-
-		let progress_bar = ProgressBar::new((opt.ntests as u64).pow(2));
-		progress_bar.set_style(
-			ProgressStyle::default_bar().template(
-				"{elapsed_precise}  {eta}  {per_sec}  {pos}/{len}  {percent}% {wide_bar}",
-			),
-		);
-
-		let mut i = 0usize;
-		let mut bf1 = BruteForce::new(CHARSET);
-		let mut buf = Vec::<String>::new();
-		progress_bar.tick();
-		loop {
-			while buf.len() < 16 && i < opt.ntests {
-				if let Some(s1) = bf1.next() {
-					buf.push(s1);
-					i += 1;
-				} else {
+	fn test_pubkey(&self, x: &PublicKey) -> (bool, bool) {
+		(x == unsafe { self.pubkey.assume_init() }, true)
+	}
+
+	fn test_pubkey_list(&self, x: &PublicKey) -> (bool, bool) {
+		(unsafe { self.pubkey_list.assume_init() }.contains(x), false)
+	}
+
+	fn test_pubkey_list_with_metadata(&self, x: &PublicKey) -> (bool, bool) {
+		(
+			unsafe { self.pubkey_list_with_metadata.assume_init() }.contains_key(x),
+			false,
+		)
+	}
+
+	fn test_regex(&self, x: &PublicKey) -> (bool, bool) {
+		(
+			unsafe { self.regex.assume_init() }.is_match(&x.to_base58()),
+			false,
+		)
+	}
+}
+
+fn main() {
+	let opt = MainOpt::from_args();
+	let data = InputData::new(&opt);
+
+	let tester = Tester::new(&data);
+
+	let generator = KeyPairFromSaltedPasswordGenerator::with_default_parameters();
+
+	let progress_bar = ProgressBar::new(opt.ntests as u64);
+	progress_bar.set_style(
+		ProgressStyle::default_bar()
+			.template("{elapsed_precise}  {eta}  {per_sec}  {pos}/{len}  {percent}% {wide_bar}"),
+	);
+	progress_bar.tick();
+
+	let found = RwLock::new(Vec::<(PublicKey, String, String)>::new());
+	let continue_test = RwLock::new(true);
+
+	BruteForce::new(CHARSET)
+		.enumerate()
+		.map_while(|(i, s1)| (*continue_test.read().unwrap() && i < opt.ntests).then_some(s1))
+		.par_bridge()
+		.for_each(|s1| {
+			let mut j = 0usize;
+			for s2 in BruteForce::new(CHARSET) {
+				let pubkey = generator
+					.generate(SaltedPassword::new(s1.clone(), s2.clone()))
+					.public_key();
+				let (res_match, res_stop) = (tester.test)(&tester, &pubkey);
+				if res_match {
+					let mut found = found.write().unwrap();
+					found.push((pubkey, s1.clone(), s2));
+				}
+				if res_stop {
+					let mut continue_test = continue_test.write().unwrap();
+					*continue_test = true;
+				}
+				if j >= opt.ntests {
 					break;
 				}
+				j += 1;
 			}
-			if buf.is_empty() {
-				break;
-			}
-			buf.par_iter().for_each(|s1| {
-				let mut j = 0usize;
-				let bf2 = BruteForce::new(CHARSET);
-				for s2 in bf2 {
-					let pubkey = generator
-						.generate(SaltedPassword::new(s1.clone(), s2.clone()))
-						.public_key();
-					if pubkeys.contains_key(&pubkey) {
-						let mut mutex = found.lock().unwrap();
-						mutex.push((pubkey, s1.clone(), s2));
-					}
-					if j >= opt.ntests {
-						break;
-					}
-					j += 1;
-				}
-				progress_bar.inc(opt.ntests as u64);
-			});
-			buf.clear();
-		}
+			progress_bar.inc(1u64);
+		});
 
-		let mut total_amount = 0u32;
-		let mut total_member = 0u32;
+	let found = found.into_inner().unwrap();
 
-		let found = found.lock().unwrap();
-		for line in found.iter() {
-			let data = pubkeys.get(&line.0).unwrap();
-			total_amount += data.amount;
-			if data.member {
-				total_member += 1;
+	match data {
+		InputData::PubkeyListWithMetadata(pubkey_list_with_metadata) => {
+			let mut total_amount = 0u32;
+			let mut total_member = 0u32;
+
+			for (pubkey, s1, s2) in found {
+				let metadata = pubkey_list_with_metadata.get(&pubkey).unwrap();
+				total_amount += metadata.amount;
+				if metadata.member {
+					total_member += 1;
+				}
+				println!(
+					"{}\t{}\t{}\t{}\t{}",
+					pubkey.to_base58(),
+					s1,
+					s2,
+					metadata.amount,
+					metadata.member as u8,
+				);
 			}
-			println!(
-				"{}\t{}\t{}\t{}\t{}",
-				line.0.to_base58(),
-				line.1,
-				line.2,
-				data.amount,
-				data.member as u8
-			);
-		}
 
-		eprintln!("Total amount: {}", total_amount);
-		eprintln!("Total member: {}", total_member);
-		eprintln!("");
+			eprintln!("Total amount: {}", total_amount);
+			eprintln!("Total member: {}", total_member);
+		}
+		_ => {
+			for (pubkey, s1, s2) in found {
+				println!("{}\t{}\t{}", pubkey.to_base58(), s1, s2);
+			}
+		}
 	}
+	eprintln!();
 }