Implement third-party exercises trust handling

This commit is contained in:
mo8it 2024-04-15 02:11:27 +02:00
parent c613b70363
commit 15ca847c37
5 changed files with 198 additions and 12 deletions

80
Cargo.lock generated
View file

@ -253,6 +253,27 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" 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]] [[package]]
name = "doc-comment" name = "doc-comment"
version = "0.3.3" version = "0.3.3"
@ -320,6 +341,17 @@ dependencies = [
"toml_edit", "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]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.14.3" version = "0.14.3"
@ -428,6 +460,16 @@ version = "0.2.153"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" 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]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.4.13" version = "0.4.13"
@ -528,6 +570,12 @@ version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.12.1" version = "0.12.1"
@ -634,6 +682,17 @@ dependencies = [
"bitflags 1.3.2", "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]] [[package]]
name = "regex" name = "regex"
version = "1.10.4" version = "1.10.4"
@ -684,6 +743,7 @@ dependencies = [
"assert_cmd", "assert_cmd",
"clap", "clap",
"crossterm", "crossterm",
"dirs",
"hashbrown", "hashbrown",
"notify-debouncer-mini", "notify-debouncer-mini",
"predicates", "predicates",
@ -865,6 +925,26 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" 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]] [[package]]
name = "toml_datetime" name = "toml_datetime"
version = "0.6.5" version = "0.6.5"

View file

@ -37,6 +37,7 @@ edition.workspace = true
anyhow.workspace = true anyhow.workspace = true
clap = { version = "4.5.4", features = ["derive"] } clap = { version = "4.5.4", features = ["derive"] }
crossterm = "0.27.0" crossterm = "0.27.0"
dirs = "5.0.1"
hashbrown = "0.14.3" hashbrown = "0.14.3"
notify-debouncer-mini = "0.4.1" notify-debouncer-mini = "0.4.1"
ratatui = "0.26.1" ratatui = "0.26.1"

View file

@ -6,7 +6,7 @@ use std::{
path::Path, 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<()> { fn create_cargo_toml(exercise_infos: &[ExerciseInfo]) -> io::Result<()> {
let mut cargo_toml = Vec::with_capacity(1 << 13); 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`")?; create_vscode_dir().context("Failed to create the file `rustlings/.vscode/extensions.json`")?;
trust_current_dir()?;
Ok(()) Ok(())
} }

View file

@ -1,5 +1,4 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use app_state::StateFileStatus;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use crossterm::{ use crossterm::{
terminal::{Clear, ClearType}, terminal::{Clear, ClearType},
@ -19,14 +18,16 @@ mod init;
mod list; mod list;
mod progress_bar; mod progress_bar;
mod run; mod run;
mod trust;
mod watch; mod watch;
use self::{ use self::{
app_state::AppState, app_state::{AppState, StateFileStatus},
info_file::InfoFile, info_file::InfoFile,
init::init, init::init,
list::list, list::list,
run::run, run::run,
trust::{current_dir_is_trusted, trust_current_dir},
watch::{watch, WatchExit}, watch::{watch, WatchExit},
}; };
@ -61,6 +62,11 @@ enum Subcommands {
/// The name of the exercise /// The name of the exercise
name: String, 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<()> { fn main() -> Result<()> {
@ -72,14 +78,26 @@ fn main() -> Result<()> {
if matches!(args.command, Some(Subcommands::Init)) { if matches!(args.command, Some(Subcommands::Init)) {
init(&info_file.exercises).context("Initialization failed")?; init(&info_file.exercises).context("Initialization failed")?;
println!("{POST_INIT_MSG}"); println!("{POST_INIT_MSG}");
return Ok(()); return Ok(());
} else if !Path::new("exercises").is_dir() { }
if !Path::new("exercises").is_dir() {
println!("{PRE_INIT_MSG}"); println!("{PRE_INIT_MSG}");
exit(1); 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( let (mut app_state, state_file_status) = AppState::new(
info_file.exercises, info_file.exercises,
info_file.final_message.unwrap_or_default(), 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 }) => { Some(Subcommands::Run { name }) => {
if let Some(name) = name { if let Some(name) = name {
app_state.set_current_exercise_by_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)?; app_state.set_current_exercise_by_name(&name)?;
println!("{}", app_state.current_exercise().hint); println!("{}", app_state.current_exercise().hint);
} }
// `Init` and `Trust` are handled above.
Some(Subcommands::Init | Subcommands::Trust) => (),
} }
Ok(()) Ok(())
@ -158,8 +176,13 @@ const CARGO_NOT_FOUND_ERR: &str = "Failed to find `cargo`.
Did you already install Rust? Did you already install Rust?
Try running `cargo --version` to diagnose the problem."; 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" 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. 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."; If you are just starting with Rustlings, run the command `rustlings init` to initialize it.";
const POST_INIT_MSG: &str = " const POST_TRUST_MSG: &str = "You now trust the exercises in the current directory.
Done initialization! Run `rustlings` to start working on them.";
Run `cd rustlings` to go into the generated directory. const NOT_TRUSTED_MSG: &str = "It looks like you are trying to work on third-party exercises.
Then run `rustlings` to get started."; 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 = "+----------------------------------------------------+ const FENISH_LINE: &str = "+----------------------------------------------------+
| You made it to the Fe-nish line! | | You made it to the Fe-nish line! |

72
src/trust.rs Normal file
View file

@ -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<bool> {
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)
}