diff --git a/Cargo.lock b/Cargo.lock index 4820df75ca..ee0b366216 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -634,6 +634,16 @@ dependencies = [ "itertools 0.10.1", ] +[[package]] +name = "combine" +version = "4.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a604e93b79d1808327a6fca85a6f2d69de66461e7620f5a4cbf5fb4d1d7c948" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "config" version = "0.11.0" @@ -2443,6 +2453,7 @@ dependencies = [ "serde 1.0.130", "serde_yaml", "tempfile", + "toml_edit", "walkdir", ] @@ -2452,9 +2463,11 @@ version = "0.1.0" dependencies = [ "anyhow", "difference", + "dirs-next", "hex", "move-core-types", "num-bigint 0.4.0", + "once_cell", "serde 1.0.130", "sha2", "walkdir", @@ -5171,6 +5184,18 @@ dependencies = [ "serde 1.0.130", ] +[[package]] +name = "toml_edit" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5376256e44f2443f8896ac012507c19a012df0fe8758b55246ae51a2279db51f" +dependencies = [ + "combine", + "indexmap", + "itertools 0.10.1", + "serde 1.0.130", +] + [[package]] name = "tracing" version = "0.1.26" diff --git a/language/move-command-line-common/Cargo.toml b/language/move-command-line-common/Cargo.toml index 361d3431c2..3804ad44ce 100644 --- a/language/move-command-line-common/Cargo.toml +++ b/language/move-command-line-common/Cargo.toml @@ -12,10 +12,12 @@ edition = "2021" [dependencies] anyhow = "1.0.52" difference = "2.0.0" +dirs-next = "2.0.0" walkdir = "2.3.1" sha2 = "0.9.3" hex = "0.4.3" num-bigint = "0.4.0" +once_cell = "1.7.2" serde = { version = "1.0.124", features = ["derive"] } move-core-types = { path = "../move-core/types" } diff --git a/language/move-command-line-common/src/env.rs b/language/move-command-line-common/src/env.rs index 888a01cca3..ac247159c8 100644 --- a/language/move-command-line-common/src/env.rs +++ b/language/move-command-line-common/src/env.rs @@ -2,6 +2,8 @@ // Copyright (c) The Move Contributors // SPDX-License-Identifier: Apache-2.0 +use once_cell::sync::Lazy; + /// An environment variable which can be set to cause the move compiler to generate /// file formats at a given version. Only version v5 and greater are supported. const BYTECODE_VERSION_ENV_VAR: &str = "MOVE_BYTECODE_VERSION"; @@ -22,3 +24,15 @@ pub fn read_bool_env_var(v: &str) -> bool { let val = read_env_var(v).to_lowercase(); val.parse::() == Ok(true) || val.parse::() == Ok(1) } + +pub static MOVE_HOME: Lazy = Lazy::new(|| { + std::env::var("MOVE_HOME").unwrap_or_else(|_| { + format!( + "{}/.move", + dirs_next::home_dir() + .expect("user's home directory not found") + .to_str() + .unwrap() + ) + }) +}); diff --git a/language/move-command-line-common/src/lib.rs b/language/move-command-line-common/src/lib.rs index 5bb4087c9a..201829bad2 100644 --- a/language/move-command-line-common/src/lib.rs +++ b/language/move-command-line-common/src/lib.rs @@ -8,6 +8,7 @@ pub mod address; pub mod character_sets; pub mod env; pub mod files; +pub mod movey_constants; pub mod parser; pub mod testing; pub mod types; diff --git a/language/move-command-line-common/src/movey_constants.rs b/language/move-command-line-common/src/movey_constants.rs new file mode 100644 index 0000000000..4567bdbf4d --- /dev/null +++ b/language/move-command-line-common/src/movey_constants.rs @@ -0,0 +1,7 @@ +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(debug_assertions)] +pub const MOVEY_URL: &str = "https://movey-app-staging.herokuapp.com"; +#[cfg(not(debug_assertions))] +pub const MOVEY_URL: &str = "https://www.movey.net"; diff --git a/language/tools/move-cli/Cargo.toml b/language/tools/move-cli/Cargo.toml index efaca163b1..e060a4133e 100644 --- a/language/tools/move-cli/Cargo.toml +++ b/language/tools/move-cli/Cargo.toml @@ -21,6 +21,8 @@ tempfile = "3.2.0" walkdir = "2.3.1" codespan-reporting = "0.11.1" itertools = "0.10.0" +toml_edit = { version = "0.14.3", features = ["easy"] } + bcs = "0.1.2" move-bytecode-verifier = { path = "../../move-bytecode-verifier" } move-disassembler = { path = "../move-disassembler" } diff --git a/language/tools/move-cli/src/base/mod.rs b/language/tools/move-cli/src/base/mod.rs index 0633fcc2dd..03b0d566c3 100644 --- a/language/tools/move-cli/src/base/mod.rs +++ b/language/tools/move-cli/src/base/mod.rs @@ -7,6 +7,7 @@ pub mod disassemble; pub mod docgen; pub mod errmap; pub mod info; +pub mod movey_login; pub mod new; pub mod prove; pub mod test; diff --git a/language/tools/move-cli/src/base/movey_login.rs b/language/tools/move-cli/src/base/movey_login.rs new file mode 100644 index 0000000000..214f5ae9c6 --- /dev/null +++ b/language/tools/move-cli/src/base/movey_login.rs @@ -0,0 +1,238 @@ +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::{bail, Result}; +use clap::Parser; +use move_command_line_common::{env::MOVE_HOME, movey_constants::MOVEY_URL}; +use std::{fs, fs::File, io, path::PathBuf}; +use toml_edit::easy::{map::Map, Value}; + +pub const MOVEY_CREDENTIAL_PATH: &str = "/movey_credential.toml"; + +#[derive(Parser)] +#[clap(name = "movey-login")] +pub struct MoveyLogin; + +impl MoveyLogin { + pub fn execute(self) -> Result<()> { + println!( + "Please paste the API Token found on {}/settings/tokens below", + MOVEY_URL + ); + let mut line = String::new(); + loop { + match io::stdin().read_line(&mut line) { + Ok(_) => { + line = line.trim().to_string(); + if !line.is_empty() { + break; + } + println!("Invalid API Token. Try again!"); + } + Err(err) => { + bail!("Error reading file: {}", err); + } + } + } + Self::save_credential(line, MOVE_HOME.clone())?; + println!("Token for Movey saved."); + Ok(()) + } + + pub fn save_credential(token: String, move_home: String) -> Result<()> { + fs::create_dir_all(&move_home)?; + let credential_path = move_home + MOVEY_CREDENTIAL_PATH; + let credential_file = PathBuf::from(&credential_path); + if !credential_file.exists() { + create_credential_file(&credential_path)?; + } + + let old_contents: String; + match fs::read_to_string(&credential_path) { + Ok(contents) => { + old_contents = contents; + } + Err(error) => bail!("Error reading input: {}", error), + } + let mut toml: Value = old_contents.parse().map_err(|e| { + anyhow::Error::from(e).context(format!( + "could not parse input at {} as TOML", + &credential_path + )) + })?; + + // only update token key, keep the rest of the file intact + if let Some(registry) = toml.as_table_mut().unwrap().get_mut("registry") { + if let Some(toml_token) = registry.as_table_mut().unwrap().get_mut("token") { + *toml_token = Value::String(token); + } else { + registry + .as_table_mut() + .unwrap() + .insert(String::from("token"), Value::String(token)); + } + } else { + let mut value = Map::new(); + value.insert(String::from("token"), Value::String(token)); + toml.as_table_mut() + .unwrap() + .insert(String::from("registry"), Value::Table(value)); + } + + let new_contents = toml.to_string(); + fs::write(credential_file, new_contents).expect("Unable to write file"); + Ok(()) + } +} + +#[cfg(unix)] +fn create_credential_file(credential_path: &str) -> Result<()> { + use std::os::unix::fs::PermissionsExt; + let credential_file = File::create(&credential_path)?; + + let mut perms = credential_file.metadata()?.permissions(); + perms.set_mode(0o600); + credential_file.set_permissions(perms)?; + Ok(()) +} + +#[cfg(windows)] +#[allow(unused)] +fn create_credential_file(credential_path: &str) -> Result<()> { + let windows_path = credential_path.replace("/", "\\"); + File::create(&windows_path)?; + Ok(()) +} + +#[cfg(not(any(unix, windows)))] +#[allow(unused)] +fn create_credential_file(credential_path: &str) -> Result<()> { + bail!("OS not supported") +} + +#[cfg(test)] +mod tests { + use super::*; + use std::env; + + fn setup_move_home(test_path: &str) -> (String, String) { + let cwd = env::current_dir().unwrap(); + let mut move_home: String = String::from(cwd.to_string_lossy()); + if !test_path.is_empty() { + move_home.push_str(&test_path); + } else { + move_home.push_str("/test"); + } + let credential_path = move_home.clone() + MOVEY_CREDENTIAL_PATH; + (move_home, credential_path) + } + + fn clean_up(move_home: &str) { + let _ = fs::remove_dir_all(move_home); + } + + #[test] + fn save_credential_works_if_no_credential_file_exists() { + let (move_home, credential_path) = + setup_move_home("/save_credential_works_if_no_credential_file_exists"); + let _ = fs::remove_dir_all(&move_home); + MoveyLogin::save_credential(String::from("test_token"), move_home.clone()).unwrap(); + + let contents = fs::read_to_string(&credential_path).expect("Unable to read file"); + let mut toml: Value = contents.parse().unwrap(); + let registry = toml.as_table_mut().unwrap().get_mut("registry").unwrap(); + let token = registry.as_table_mut().unwrap().get_mut("token").unwrap(); + assert!(token.to_string().contains("test_token")); + + clean_up(&move_home); + } + + #[test] + fn save_credential_works_if_empty_credential_file_exists() { + let (move_home, credential_path) = + setup_move_home("/save_credential_works_if_empty_credential_file_exists"); + + let _ = fs::remove_dir_all(&move_home); + fs::create_dir_all(&move_home).unwrap(); + File::create(&credential_path).unwrap(); + + let contents = fs::read_to_string(&credential_path).expect("Unable to read file"); + let mut toml: Value = contents.parse().unwrap(); + assert!(toml.as_table_mut().unwrap().get_mut("registry").is_none()); + + MoveyLogin::save_credential(String::from("test_token"), move_home.clone()).unwrap(); + + let contents = fs::read_to_string(&credential_path).expect("Unable to read file"); + let mut toml: Value = contents.parse().unwrap(); + let registry = toml.as_table_mut().unwrap().get_mut("registry").unwrap(); + let token = registry.as_table_mut().unwrap().get_mut("token").unwrap(); + assert!(token.to_string().contains("test_token")); + + clean_up(&move_home); + } + + #[test] + fn save_credential_works_if_token_field_exists() { + let (move_home, credential_path) = + setup_move_home("/save_credential_works_if_token_field_exists"); + + let _ = fs::remove_dir_all(&move_home); + fs::create_dir_all(&move_home).unwrap(); + File::create(&credential_path).unwrap(); + + let old_content = + String::from("[registry]\ntoken = \"old_test_token\"\nversion = \"0.0.0\"\n"); + fs::write(&credential_path, old_content).expect("Unable to write file"); + + let contents = fs::read_to_string(&credential_path).expect("Unable to read file"); + let mut toml: Value = contents.parse().unwrap(); + let registry = toml.as_table_mut().unwrap().get_mut("registry").unwrap(); + let token = registry.as_table_mut().unwrap().get_mut("token").unwrap(); + assert!(token.to_string().contains("old_test_token")); + assert!(!token.to_string().contains("new_world")); + + MoveyLogin::save_credential(String::from("new_world"), move_home.clone()).unwrap(); + + let contents = fs::read_to_string(&credential_path).expect("Unable to read file"); + let mut toml: Value = contents.parse().unwrap(); + let registry = toml.as_table_mut().unwrap().get_mut("registry").unwrap(); + let token = registry.as_table_mut().unwrap().get_mut("token").unwrap(); + assert!(token.to_string().contains("new_world")); + assert!(!token.to_string().contains("old_test_token")); + let version = registry.as_table_mut().unwrap().get_mut("version").unwrap(); + assert!(version.to_string().contains("0.0.0")); + + clean_up(&move_home); + } + + #[test] + fn save_credential_works_if_empty_token_field_exists() { + let (move_home, credential_path) = + setup_move_home("/save_credential_works_if_empty_token_field_exists"); + + let _ = fs::remove_dir_all(&move_home); + fs::create_dir_all(&move_home).unwrap(); + File::create(&credential_path).unwrap(); + + let old_content = String::from("[registry]\ntoken = \"\"\nversion = \"0.0.0\"\n"); + fs::write(&credential_path, old_content).expect("Unable to write file"); + + let contents = fs::read_to_string(&credential_path).expect("Unable to read file"); + let mut toml: Value = contents.parse().unwrap(); + let registry = toml.as_table_mut().unwrap().get_mut("registry").unwrap(); + let token = registry.as_table_mut().unwrap().get_mut("token").unwrap(); + assert!(!token.to_string().contains("test_token")); + + MoveyLogin::save_credential(String::from("test_token"), move_home.clone()).unwrap(); + + let contents = fs::read_to_string(&credential_path).expect("Unable to read file"); + let mut toml: Value = contents.parse().unwrap(); + let registry = toml.as_table_mut().unwrap().get_mut("registry").unwrap(); + let token = registry.as_table_mut().unwrap().get_mut("token").unwrap(); + assert!(token.to_string().contains("test_token")); + let version = registry.as_table_mut().unwrap().get_mut("version").unwrap(); + assert!(version.to_string().contains("0.0.0")); + + clean_up(&move_home); + } +} diff --git a/language/tools/move-cli/src/lib.rs b/language/tools/move-cli/src/lib.rs index e3206a904c..f7cef69805 100644 --- a/language/tools/move-cli/src/lib.rs +++ b/language/tools/move-cli/src/lib.rs @@ -4,7 +4,7 @@ use base::{ build::Build, coverage::Coverage, disassemble::Disassemble, docgen::Docgen, errmap::Errmap, - info::Info, new::New, prove::Prove, test::Test, + info::Info, movey_login::MoveyLogin, new::New, prove::Prove, test::Test, }; use move_package::BuildConfig; @@ -91,6 +91,8 @@ pub enum Command { #[clap(subcommand)] cmd: experimental::cli::ExperimentalCommand, }, + #[clap(name = "movey-login")] + MoveyLogin(MoveyLogin), } pub fn run_cli( @@ -121,6 +123,7 @@ pub fn run_cli( &storage_dir, ), Command::Experimental { storage_dir, cmd } => cmd.handle_command(&move_args, &storage_dir), + Command::MoveyLogin(c) => c.execute(), } } diff --git a/language/tools/move-cli/tests/cli_tests.rs b/language/tools/move-cli/tests/cli_tests.rs index b77954862b..f0c6d060c2 100644 --- a/language/tools/move-cli/tests/cli_tests.rs +++ b/language/tools/move-cli/tests/cli_tests.rs @@ -2,9 +2,16 @@ // Copyright (c) The Move Contributors // SPDX-License-Identifier: Apache-2.0 -use move_cli::sandbox::commands::test; +use move_cli::{base::movey_login::MOVEY_CREDENTIAL_PATH, sandbox::commands::test}; +use move_command_line_common::movey_constants::MOVEY_URL; +#[cfg(unix)] +use std::fs::File; +use std::{env, fs, io::Write}; -use std::path::PathBuf; +#[cfg(unix)] +use std::os::unix::fs::PermissionsExt; +use std::{path::PathBuf, process::Stdio}; +use toml_edit::easy::Value; pub const CLI_METATEST_PATH: [&str; 3] = ["tests", "metatests", "args.txt"]; @@ -53,3 +60,116 @@ fn cross_process_locking_git_deps() { .expect("Package2 failed"); handle.join().unwrap(); } + +#[test] +fn save_credential_works() { + let cli_exe = env!("CARGO_BIN_EXE_move"); + let (move_home, credential_path) = setup_move_home("/save_credential_works"); + assert!(fs::read_to_string(&credential_path).is_err()); + + match std::process::Command::new(cli_exe) + .env("MOVE_HOME", &move_home) + .current_dir(".") + .args(["movey-login"]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + { + Ok(child) => { + let token = "test_token"; + child + .stdin + .as_ref() + .unwrap() + .write_all(token.as_bytes()) + .unwrap(); + match child.wait_with_output() { + Ok(output) => { + assert!(String::from_utf8_lossy(&output.stdout).contains(&format!( + "Please paste the API Token found on {}/settings/tokens below", + MOVEY_URL + ))); + Ok(()) + } + Err(error) => Err(error), + } + } + Err(error) => Err(error), + } + .unwrap(); + + let contents = fs::read_to_string(&credential_path).expect("Unable to read file"); + let mut toml: Value = contents.parse().unwrap(); + let registry = toml.as_table_mut().unwrap().get_mut("registry").unwrap(); + let token = registry.as_table_mut().unwrap().get_mut("token").unwrap(); + assert!(token.to_string().contains("test_token")); + + clean_up(&move_home) +} + +#[cfg(unix)] +#[test] +fn save_credential_fails_if_undeletable_credential_file_exists() { + let cli_exe = env!("CARGO_BIN_EXE_move"); + let (move_home, credential_path) = + setup_move_home("/save_credential_fails_if_undeletable_credential_file_exists"); + let file = File::create(&credential_path).unwrap(); + let mut perms = file.metadata().unwrap().permissions(); + perms.set_mode(0o000); + file.set_permissions(perms).unwrap(); + + match std::process::Command::new(cli_exe) + .env("MOVE_HOME", &move_home) + .current_dir(".") + .args(["movey-login"]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + { + Ok(child) => { + let token = "test_token"; + child + .stdin + .as_ref() + .unwrap() + .write_all(token.as_bytes()) + .unwrap(); + match child.wait_with_output() { + Ok(output) => { + assert!(String::from_utf8_lossy(&output.stdout).contains(&format!( + "Please paste the API Token found on {}/settings/tokens below", + MOVEY_URL + ))); + assert!(String::from_utf8_lossy(&output.stderr) + .contains("Error: Error reading input: Permission denied (os error 13)")); + Ok(()) + } + Err(error) => Err(error), + } + } + Err(error) => Err(error), + } + .unwrap(); + + let mut perms = file.metadata().unwrap().permissions(); + perms.set_mode(0o600); + file.set_permissions(perms).unwrap(); + let _ = fs::remove_file(&credential_path); + + clean_up(&move_home) +} + +fn setup_move_home(test_path: &str) -> (String, String) { + let cwd = env::current_dir().unwrap(); + let mut move_home: String = String::from(cwd.to_string_lossy()); + move_home.push_str(&test_path); + let _ = fs::remove_dir_all(&move_home); + fs::create_dir_all(&move_home).unwrap(); + let credential_path = move_home.clone() + MOVEY_CREDENTIAL_PATH; + (move_home, credential_path) +} + +fn clean_up(move_home: &str) { + let _ = fs::remove_dir_all(move_home); +} diff --git a/language/tools/move-package/src/source_package/manifest_parser.rs b/language/tools/move-package/src/source_package/manifest_parser.rs index 69f17cce68..e6139c1d67 100644 --- a/language/tools/move-package/src/source_package/manifest_parser.rs +++ b/language/tools/move-package/src/source_package/manifest_parser.rs @@ -4,6 +4,7 @@ use crate::{source_package::parsed_manifest as PM, Architecture}; use anyhow::{bail, format_err, Context, Result}; +use move_command_line_common::env::MOVE_HOME; use move_core_types::account_address::{AccountAddress, AccountAddressParseError}; use move_symbol_pool::symbol::Symbol; use std::{ @@ -328,15 +329,7 @@ fn parse_dependency(tval: TV) -> Result { } (None, Some(git)) => { // Look to see if a MOVE_HOME has been set. Otherwise default to $HOME - let move_home = std::env::var("MOVE_HOME").unwrap_or_else(|_| { - format!( - "{}/.move", - dirs_next::home_dir() - .expect("user's home directory not found") - .to_str() - .unwrap() - ) - }); + let move_home = MOVE_HOME.clone(); let rev_name = match table.remove("rev") { None => bail!("Git revision not supplied for dependency"), Some(r) => Symbol::from(