Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tab completion and syntax checking #261

Merged
merged 66 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
585f8e6
clipboard as a feature for android and server support
kernelPanic0x Sep 5, 2024
73790ef
cargo fmt
kernelPanic0x Sep 5, 2024
cb72ef0
improved wormhole code display for serve and send
kernelPanic0x Sep 5, 2024
b1c7928
cargo fmt
kernelPanic0x Sep 6, 2024
39585e8
clipboard as default feature
kernelPanic0x Sep 10, 2024
4affcc9
improved clipboard conditional wormhole code message
kernelPanic0x Sep 10, 2024
63e92c7
added whitespace for readability
kernelPanic0x Sep 10, 2024
d99d120
Merge remote-tracking branch 'upstream/main' into magic-wormhole-main
kernelPanic0x Sep 11, 2024
06b6c50
Merge remote-tracking branch 'upstream/main'
kernelPanic0x Sep 16, 2024
049bca7
first working draft of code word spelling checks
kernelPanic0x Sep 19, 2024
39224e6
removed regex as it is not needed anymore
kernelPanic0x Sep 19, 2024
f148556
improved coloring
kernelPanic0x Sep 19, 2024
45f7b7f
first working tab completion
kernelPanic0x Sep 20, 2024
99bcd64
working tab complete
kernelPanic0x Sep 20, 2024
c2a294e
temporary ctrlc workaround
kernelPanic0x Sep 20, 2024
98f948c
improved channel code checks
kernelPanic0x Sep 21, 2024
cddb3fa
completer optimizations
kernelPanic0x Sep 21, 2024
bbdcdbc
working tab completion with jaro winkler algorhythm
kernelPanic0x Sep 22, 2024
87c4fda
added documentation for wordlist module, allow missing_docs for core …
kernelPanic0x Sep 22, 2024
4fba926
bail on error while reading wormhole code
kernelPanic0x Sep 22, 2024
6ff966d
clipboard as a feature for android and server support
kernelPanic0x Sep 5, 2024
f450a27
added whitespace for readability
kernelPanic0x Sep 10, 2024
8ba3e69
first working draft of code word spelling checks
kernelPanic0x Sep 19, 2024
7abbb50
removed regex as it is not needed anymore
kernelPanic0x Sep 19, 2024
ba6ce02
improved coloring
kernelPanic0x Sep 19, 2024
7806217
first working tab completion
kernelPanic0x Sep 20, 2024
c80520d
working tab complete
kernelPanic0x Sep 20, 2024
9b0d6c9
temporary ctrlc workaround
kernelPanic0x Sep 20, 2024
4abfb29
improved channel code checks
kernelPanic0x Sep 21, 2024
d269554
completer optimizations
kernelPanic0x Sep 21, 2024
9c3a26b
working tab completion with jaro winkler algorhythm
kernelPanic0x Sep 22, 2024
2e7f038
added documentation for wordlist module, allow missing_docs for core …
kernelPanic0x Sep 22, 2024
487733b
bail on error while reading wormhole code
kernelPanic0x Sep 22, 2024
1564f9e
Merge branch 'tab_complete' of github.com:kernelPanic0x/magic-wormhol…
kernelPanic0x Sep 23, 2024
6782f5f
fixed index out of range crash when suggestion > current word, added …
kernelPanic0x Sep 23, 2024
6a13436
removed temporary documentation, fixed clippy::never_loop
kernelPanic0x Sep 23, 2024
e83e96f
fixed typo
kernelPanic0x Sep 24, 2024
ec02d66
wormhole code validation length check
kernelPanic0x Sep 25, 2024
cbfbf80
wormhole code validation length check, use defaults for fuzzy search
kernelPanic0x Sep 25, 2024
0ab6560
use std::sync::LazyLock instead of lazy_static
kernelPanic0x Oct 4, 2024
71ec08a
use 0.8 confidence for menu system to work
kernelPanic0x Oct 4, 2024
a958cdd
fixed docs for get_n_top
kernelPanic0x Oct 4, 2024
0fe7823
made wordlist public without exposing core
kernelPanic0x Oct 4, 2024
55b57cd
span_end using min
kernelPanic0x Oct 4, 2024
4210dce
removed partial completions
kernelPanic0x Oct 4, 2024
c9b2846
fixed typo
kernelPanic0x Oct 5, 2024
1007c8c
removed unnecessary comment
kernelPanic0x Oct 5, 2024
af7b17c
Merge branch 'tab_complete' of github.com:kernelPanic0x/magic-wormhol…
kernelPanic0x Oct 5, 2024
bd19d09
removed code highlighting because of custom wordlists
kernelPanic0x Oct 7, 2024
0e349cd
rust version 1.81 for LazyLock support
kernelPanic0x Oct 7, 2024
5cfdd41
removed code highlighting because of custom wordlists
kernelPanic0x Oct 7, 2024
cbb3caa
rust version 1.81 for LazyLock support
kernelPanic0x Oct 7, 2024
6fb8f86
dialoguer tab completion implementation
kernelPanic0x Oct 8, 2024
e7fefce
added complter, removed reedline crate
kernelPanic0x Oct 8, 2024
f341998
added documentation
kernelPanic0x Oct 8, 2024
ceeeef3
documentation, cleanup
kernelPanic0x Oct 8, 2024
711875a
removed fuzzt from cli
kernelPanic0x Oct 8, 2024
585a198
Merge branch 'tab_complete' of github.com:kernelPanic0x/magic-wormhol…
kernelPanic0x Oct 8, 2024
ece999b
removed reedline
kernelPanic0x Oct 9, 2024
3ccb0f4
fuzzy-complete feature tests
kernelPanic0x Oct 9, 2024
afa5090
fixed conditional compile errors
kernelPanic0x Oct 10, 2024
99dc318
made helper functions private
kernelPanic0x Oct 11, 2024
86abf9d
merged with upstream
kernelPanic0x Oct 11, 2024
3dead09
fixed compilation error with --no-default-features
kernelPanic0x Oct 11, 2024
faff3cd
fixed compilation error with --no-default-features
kernelPanic0x Oct 11, 2024
ca5d292
fixed wrong function for normal_complete
kernelPanic0x Oct 11, 2024
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
7 changes: 7 additions & 0 deletions Cargo.lock

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

8 changes: 5 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ homepage = "http://magic-wormhole.io/"
repository = "https://github.com/magic-wormhole/magic-wormhole.rs/tree/main/cli"
license = "EUPL-1.2"

rust-version = "1.75"
rust-version = "1.81"
edition = "2021"

[workspace.dependencies]
Expand All @@ -38,7 +38,6 @@ futures = "0.3.12"
hex = "0.4.2"
hkdf = "0.12.2"
indicatif = "0.17.0"
log = "0.4.13"
noise-protocol = "0.2"
noise-rust-crypto = "0.6.0-rc.1"
number_prefix = "0.4.0"
Expand Down Expand Up @@ -107,6 +106,8 @@ zxcvbn = { workspace = true, optional = true }

tracing = { workspace = true, features = ["log", "log-always"] }

fuzzt = { version = "0.3.1", optional = true }

# Transit dependencies


Expand Down Expand Up @@ -162,7 +163,7 @@ transit = [
"dep:async-trait",
]
forwarding = ["transit", "dep:rmp-serde"]
default = ["transit", "transfer"]
default = ["transit", "transfer", "fuzzy-complete"]
all = ["default", "forwarding"]

# TLS implementations for websocket connections via async-tungstenite
Expand All @@ -173,6 +174,7 @@ native-tls = ["async-tungstenite/async-native-tls"]
# By enabling this option you are opting out of semver stability.
experimental-transfer-v2 = []
experimental = ["experimental-transfer-v2"]
fuzzy-complete = ["fuzzt"]

[profile.release]
overflow-checks = true
Expand Down
2 changes: 1 addition & 1 deletion cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ clap_complete = { workspace = true }
env_logger = { workspace = true }
console = { workspace = true }
indicatif = { workspace = true }
dialoguer = { workspace = true }
dialoguer = { workspace = true, features = ["completion"] }
color-eyre = { workspace = true }
number_prefix = { workspace = true }
ctrlc = { workspace = true }
Expand Down
31 changes: 31 additions & 0 deletions cli/src/completer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use std::sync::LazyLock;

use color_eyre::eyre;
use dialoguer::{Completion, Input};
use magic_wormhole::wordlist::{default_wordlist, Wordlist};

static WORDLIST: LazyLock<Wordlist> = LazyLock::new(|| default_wordlist(2));

struct CustomCompletion {}

impl CustomCompletion {
pub fn default() -> Self {
CustomCompletion {}
}
}

impl Completion for CustomCompletion {
fn get(&self, input: &str) -> Option<String> {
WORDLIST.get_completions(input).first().cloned()
}
}

pub fn enter_code() -> eyre::Result<String> {
let custom_completion = CustomCompletion::default();

Input::new()
.with_prompt("Wormhole Code")
.completion_with(&custom_completion)
.interact_text()
.map_err(From::from)
}
11 changes: 2 additions & 9 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#![allow(clippy::too_many_arguments)]
mod completer;
mod util;

use std::{
Expand All @@ -9,6 +10,7 @@ use std::{
use async_std::sync::Arc;
use clap::{Args, CommandFactory, Parser, Subcommand};
use color_eyre::{eyre, eyre::Context};
use completer::enter_code;
use console::{style, Term};
use futures::{future::Either, Future, FutureExt};
use indicatif::{MultiProgress, ProgressBar};
Expand Down Expand Up @@ -750,15 +752,6 @@ fn create_progress_handler(pb: ProgressBar) -> impl FnMut(u64, u64) {
}
}

fn enter_code() -> eyre::Result<String> {
use dialoguer::Input;

Input::new()
.with_prompt("Enter code")
.interact_text()
.map_err(From::from)
}

fn print_welcome(term: &mut Term, welcome: Option<&str>) -> eyre::Result<()> {
if let Some(welcome) = &welcome {
writeln!(term, "Got welcome from server: {}", welcome)?;
Expand Down
4 changes: 3 additions & 1 deletion src/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ pub mod rendezvous;
mod server_messages;
#[cfg(test)]
mod test;
mod wordlist;

/// Module for wormhole code generation and completion.
pub mod wordlist;

use serde_derive::{Deserialize, Serialize};
use std::{borrow::Cow, str::FromStr};
Expand Down
169 changes: 106 additions & 63 deletions src/core/wordlist.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
///! Wordlist generation and wormhole code utilities
use rand::{rngs::OsRng, seq::SliceRandom};
use serde_json::{self, Value};
use std::fmt;

use super::Password;

/// Represents a list of words used to generate and complete wormhole codes.
/// A wormhole code is a sequence of words used for secure communication or identification.
#[derive(PartialEq)]
pub struct Wordlist {
kernelPanic0x marked this conversation as resolved.
Show resolved Hide resolved
pub num_words: usize,
/// Number of words in a wormhole code
num_words: usize,
/// Odd and even wordlist
words: Vec<Vec<String>>,
}

Expand All @@ -18,45 +23,67 @@ impl fmt::Debug for Wordlist {

impl Wordlist {
#[cfg(test)]
#[doc(hidden)]
pub fn new(num_words: usize, words: Vec<Vec<String>>) -> Wordlist {
Wordlist { num_words, words }
}

#[allow(dead_code)] // TODO make this API public one day
/// Completes a wormhole code
///
/// Completion can be done either with fuzzy search (approximate string matching)
/// or simple `starts_with` matching.
pub fn get_completions(&self, prefix: &str) -> Vec<String> {
let count_dashes = prefix.matches('-').count();
let mut completions = Vec::new();
let words = &self.words[count_dashes % self.words.len()];

let last_partial_word = prefix.split('-').last();
let lp = if let Some(w) = last_partial_word {
w.len()
} else {
0
};

for word in words {
let mut suffix: String = prefix.to_owned();
if word.starts_with(last_partial_word.unwrap()) {
if lp == 0 {
suffix.push_str(word);
} else {
let p = prefix.len() - lp;
suffix.truncate(p);
suffix.push_str(word);
let words = self.get_wordlist(prefix);

let (prefix_without_last, last_partial) = prefix.rsplit_once('-').unwrap_or(("", prefix));

#[cfg(feature = "fuzzy-complete")]
let matches = self.fuzzy_complete(last_partial, words);
#[cfg(not(feature = "fuzzy-complete"))]
let matches = self.normal_complete(last_partial, words);

matches
.into_iter()
.map(|word| {
let mut completion = String::new();
completion.push_str(prefix_without_last);
if !prefix_without_last.is_empty() {
completion.push('-');
}
completion.push_str(&word);
completion
})
.collect()
}

if count_dashes + 1 < self.num_words {
suffix.push('-');
}
fn get_wordlist(&self, prefix: &str) -> &Vec<String> {
let count_dashes = prefix.matches('-').count();
&self.words[count_dashes % self.words.len()]
}

completions.push(suffix);
}
}
completions.sort();
completions
#[cfg(feature = "fuzzy-complete")]
fn fuzzy_complete(&self, partial: &str, words: &[String]) -> Vec<String> {
// We use Jaro-Winkler algorithm because it emphasizes the beginning of a word
use fuzzt::algorithms::JaroWinkler;

let words = words.iter().map(|w| w.as_str()).collect::<Vec<&str>>();

fuzzt::get_top_n(partial, &words, None, None, None, Some(&JaroWinkler))
.into_iter()
.map(|s| s.to_string())
.collect()
}

#[allow(unused)]
fn normal_complete(&self, partial: &str, words: &[String]) -> Vec<String> {
words
.iter()
.filter(|word| word.starts_with(partial))
.cloned()
.collect()
}

/// Choose wormhole code word
pub fn choose_words(&self) -> Password {
let mut rng = OsRng;
let components: Vec<String> = self
Expand Down Expand Up @@ -106,6 +133,7 @@ fn load_pgpwords() -> Vec<Vec<String>> {
vec![even_words, odd_words]
}

/// Construct Wordlist struct with given number of words in a wormhole code
pub fn default_wordlist(num_words: usize) -> Wordlist {
Wordlist {
num_words,
Expand Down Expand Up @@ -155,8 +183,9 @@ mod test {
];

let w = Wordlist::new(2, words);
assert_eq!(w.get_completions(""), vec!["green-", "purple-", "yellow-"]);
assert_eq!(w.get_completions("pur"), vec!["purple-"]);
assert_eq!(w.get_completions(""), Vec::<String>::new());
assert_eq!(w.get_completions("9"), Vec::<String>::new());
assert_eq!(w.get_completions("pur"), vec!["purple"]);
assert_eq!(w.get_completions("blu"), Vec::<String>::new());
assert_eq!(w.get_completions("purple-sa"), vec!["purple-sausages"]);
}
Expand Down Expand Up @@ -197,45 +226,59 @@ mod test {
}

#[test]
fn test_default_completions() {
let w = default_wordlist(2);
let c = w.get_completions("ar");
assert_eq!(c.len(), 2);
assert!(c.contains(&String::from("article-")));
assert!(c.contains(&String::from("armistice-")));
#[cfg(feature = "fuzzy-complete")]
fn test_wormhole_code_fuzzy_completions() {
let list = default_wordlist(2);

assert_eq!(list.get_completions("22"), Vec::<String>::new());
assert_eq!(list.get_completions("22-"), Vec::<String>::new());

let c = w.get_completions("armis");
assert_eq!(c.len(), 1);
assert!(c.contains(&String::from("armistice-")));
// Invalid wormhole code check
assert_eq!(list.get_completions("trj"), Vec::<String>::new());

let c = w.get_completions("armistice-");
assert_eq!(c.len(), 256);
assert_eq!(
list.get_completions("22-chisel"),
["22-chisel", "22-chairlift", "22-christmas"]
);

let c = w.get_completions("armistice-ba");
assert_eq!(
c,
vec![
"armistice-baboon",
"armistice-backfield",
"armistice-backward",
"armistice-banjo",
]
list.get_completions("22-chle"),
["22-chisel", "22-chatter", "22-checkup"]
);

let w = default_wordlist(3);
let c = w.get_completions("armistice-ba");
assert_eq!(list.get_completions("22-chisel-tba"), ["22-chisel-tobacco"]);
}

#[test]
#[cfg(feature = "fuzzy-complete")]
fn test_completion_fuzzy() {
let wl = default_wordlist(2);
let list = wl.get_wordlist("22-");

assert_eq!(wl.fuzzy_complete("chck", list), ["checkup", "choking"]);
assert_eq!(wl.fuzzy_complete("checkp", list), ["checkup"]);
assert_eq!(
c,
vec![
"armistice-baboon-",
"armistice-backfield-",
"armistice-backward-",
"armistice-banjo-",
]
wl.fuzzy_complete("checkup", list),
["checkup", "lockup", "cleanup"]
);
}

let w = default_wordlist(4);
let c = w.get_completions("armistice-baboon");
assert_eq!(c, vec!["armistice-baboon-"]);
#[test]
fn test_completion_normal() {
let wl = default_wordlist(2);
let list = wl.get_wordlist("22-");

assert_eq!(wl.normal_complete("che", list), ["checkup"]);
}

#[test]
fn test_full_wormhole_completion() {
let wl = default_wordlist(2);

assert_eq!(wl.get_completions("22-chec").first().unwrap(), "22-checkup");
assert_eq!(
wl.get_completions("22-checkup-t").first().unwrap(),
"22-checkup-tobacco"
);
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
#[macro_use]
mod util;
mod core;
pub use core::wordlist;
#[cfg(feature = "forwarding")]
pub mod forwarding;
#[cfg(feature = "transfer")]
Expand Down
Loading