From 15ca847c37c170590abe6caa53dba5606d956341 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 15 Apr 2024 02:11:27 +0200 Subject: [PATCH] Implement third-party exercises trust handling --- Cargo.lock | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/init.rs | 4 ++- src/main.rs | 53 ++++++++++++++++++++++++++-------- src/trust.rs | 72 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 198 insertions(+), 12 deletions(-) create mode 100644 src/trust.rs diff --git a/Cargo.lock b/Cargo.lock index 5cfebe60..671a03ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -253,6 +253,27 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + [[package]] name = "doc-comment" version = "0.3.3" @@ -320,6 +341,17 @@ dependencies = [ "toml_edit", ] +[[package]] +name = "getrandom" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "hashbrown" version = "0.14.3" @@ -428,6 +460,16 @@ version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.5.0", + "libc", +] + [[package]] name = "linux-raw-sys" version = "0.4.13" @@ -528,6 +570,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "parking_lot" version = "0.12.1" @@ -634,6 +682,17 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_users" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + [[package]] name = "regex" version = "1.10.4" @@ -684,6 +743,7 @@ dependencies = [ "assert_cmd", "clap", "crossterm", + "dirs", "hashbrown", "notify-debouncer-mini", "predicates", @@ -865,6 +925,26 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" +[[package]] +name = "thiserror" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.58", +] + [[package]] name = "toml_datetime" version = "0.6.5" diff --git a/Cargo.toml b/Cargo.toml index 07865abc..6cf9ef94 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ edition.workspace = true anyhow.workspace = true clap = { version = "4.5.4", features = ["derive"] } crossterm = "0.27.0" +dirs = "5.0.1" hashbrown = "0.14.3" notify-debouncer-mini = "0.4.1" ratatui = "0.26.1" diff --git a/src/init.rs b/src/init.rs index 459519de..d051fc42 100644 --- a/src/init.rs +++ b/src/init.rs @@ -6,7 +6,7 @@ use std::{ path::Path, }; -use crate::{embedded::EMBEDDED_FILES, info_file::ExerciseInfo}; +use crate::{embedded::EMBEDDED_FILES, info_file::ExerciseInfo, trust::trust_current_dir}; fn create_cargo_toml(exercise_infos: &[ExerciseInfo]) -> io::Result<()> { let mut cargo_toml = Vec::with_capacity(1 << 13); @@ -85,6 +85,8 @@ pub fn init(exercise_infos: &[ExerciseInfo]) -> Result<()> { create_vscode_dir().context("Failed to create the file `rustlings/.vscode/extensions.json`")?; + trust_current_dir()?; + Ok(()) } diff --git a/src/main.rs b/src/main.rs index ed5becf5..7b63f70f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,4 @@ use anyhow::{Context, Result}; -use app_state::StateFileStatus; use clap::{Parser, Subcommand}; use crossterm::{ terminal::{Clear, ClearType}, @@ -19,14 +18,16 @@ mod init; mod list; mod progress_bar; mod run; +mod trust; mod watch; use self::{ - app_state::AppState, + app_state::{AppState, StateFileStatus}, info_file::InfoFile, init::init, list::list, run::run, + trust::{current_dir_is_trusted, trust_current_dir}, watch::{watch, WatchExit}, }; @@ -61,6 +62,11 @@ enum Subcommands { /// The name of the exercise name: String, }, + /// Trust the current directory with its exercises. + /// + /// You only need to run this if you want to work on third-party exercises or after you moved + /// the official exercises that were initialized with `rustlings init`. + Trust, } fn main() -> Result<()> { @@ -72,14 +78,26 @@ fn main() -> Result<()> { if matches!(args.command, Some(Subcommands::Init)) { init(&info_file.exercises).context("Initialization failed")?; - println!("{POST_INIT_MSG}"); return Ok(()); - } else if !Path::new("exercises").is_dir() { + } + + if !Path::new("exercises").is_dir() { println!("{PRE_INIT_MSG}"); exit(1); } + if matches!(args.command, Some(Subcommands::Trust)) { + trust_current_dir()?; + println!("{POST_TRUST_MSG}"); + return Ok(()); + } + + if !current_dir_is_trusted()? { + println!("{NOT_TRUSTED_MSG}"); + exit(1); + } + let (mut app_state, state_file_status) = AppState::new( info_file.exercises, info_file.final_message.unwrap_or_default(), @@ -130,8 +148,6 @@ fn main() -> Result<()> { } } } - // `Init` is handled above. - Some(Subcommands::Init) => (), Some(Subcommands::Run { name }) => { if let Some(name) = name { app_state.set_current_exercise_by_name(&name)?; @@ -149,6 +165,8 @@ fn main() -> Result<()> { app_state.set_current_exercise_by_name(&name)?; println!("{}", app_state.current_exercise().hint); } + // `Init` and `Trust` are handled above. + Some(Subcommands::Init | Subcommands::Trust) => (), } Ok(()) @@ -158,8 +176,13 @@ const CARGO_NOT_FOUND_ERR: &str = "Failed to find `cargo`. Did you already install Rust? Try running `cargo --version` to diagnose the problem."; +const POST_INIT_MSG: &str = "Done initialization! + +Run `cd rustlings` to go into the generated directory. +Then run `rustlings` to get started."; + const PRE_INIT_MSG: &str = r" - welcome to... + Welcome to... _ _ _ _ __ _ _ ___| |_| (_)_ __ __ _ ___ | '__| | | / __| __| | | '_ \ / _` / __| @@ -170,11 +193,19 @@ const PRE_INIT_MSG: &str = r" The `exercises` directory wasn't found in the current directory. If you are just starting with Rustlings, run the command `rustlings init` to initialize it."; -const POST_INIT_MSG: &str = " -Done initialization! +const POST_TRUST_MSG: &str = "You now trust the exercises in the current directory. +Run `rustlings` to start working on them."; -Run `cd rustlings` to go into the generated directory. -Then run `rustlings` to get started."; +const NOT_TRUSTED_MSG: &str = "It looks like you are trying to work on third-party exercises. +Rustlings supports third-party exercises. But because Rustlings runs the code inside an exercise, +we need to warn you about the possibility of malicious code. +We recommend that you read all the exercise files in the `exercises` directory and check the +dependencies in the `Cargo.toml` file. +If everything looks fine and you want to trust this directory, run `rustlings trust`. + +If you you are trying to work on the official exercises that were generated using `rustlings init`, +then you probably moved the directory containing them. In that case, you can run `rustlings trust` +without a problem."; const FENISH_LINE: &str = "+----------------------------------------------------+ | You made it to the Fe-nish line! | diff --git a/src/trust.rs b/src/trust.rs new file mode 100644 index 00000000..7e36f737 --- /dev/null +++ b/src/trust.rs @@ -0,0 +1,72 @@ +use anyhow::{Context, Error, Result}; +use std::{ + env, + fs::{self, OpenOptions}, + io::{ErrorKind, Write}, +}; + +const DATA_DIR_NAME: &str = "rustlings"; +const TRUSTED_DIRS_FILE_NAME: &str = "trusted-dirs.txt"; + +pub fn trust_current_dir() -> Result<()> { + let mut path = dirs::data_dir().context("Failed to determine the data directory")?; + path.push(DATA_DIR_NAME); + if !path.is_dir() { + fs::create_dir(&path) + .with_context(|| format!("Failed to create the directory {}", path.display()))?; + } + + path.push(TRUSTED_DIRS_FILE_NAME); + let mut file = OpenOptions::new() + .create(true) + .append(true) + .open(&path) + .with_context(|| { + format!( + "Failed to create/open the file {} in write mode", + path.display(), + ) + })?; + + let dir = env::current_dir().context("Failed to get the current directory path")?; + let dir = dir.to_string_lossy(); + let mut line = Vec::with_capacity(dir.as_bytes().len() + 1); + line.extend_from_slice(dir.as_bytes()); + line.push(b'\n'); + + file.write_all(&line) + .with_context(|| format!("Failed to append to the file {}", path.display())) +} + +pub fn current_dir_is_trusted() -> Result { + let mut path = dirs::data_dir().context("Failed to determine the data directory")?; + path.push(DATA_DIR_NAME); + path.push(TRUSTED_DIRS_FILE_NAME); + + let content = match fs::read(&path) { + Ok(v) => v, + Err(e) => match e.kind() { + ErrorKind::NotFound => return Ok(false), + _ => { + return Err( + Error::from(e).context(format!("Failed to read the file {}", path.display())) + ) + } + }, + }; + + let current_dir = env::current_dir().context("Failed to get the current directory path")?; + let current_dir = current_dir.to_string_lossy(); + + for line in content.split(|c| *c == b'\n') { + if line.is_empty() { + break; + } + + if line == current_dir.as_bytes() { + return Ok(true); + } + } + + Ok(false) +}