2024-04-17 21:46:21 +01:00
use anyhow ::{ anyhow , bail , Context , Error , Result } ;
2024-04-17 17:19:08 +01:00
use std ::{
cmp ::Ordering ,
2024-04-17 21:46:21 +01:00
fs ::{ self , read_dir , OpenOptions } ,
io ::Read ,
path ::{ Path , PathBuf } ,
2024-04-17 17:19:08 +01:00
} ;
2024-04-15 22:54:57 +01:00
2024-04-17 14:55:50 +01:00
use crate ::{
2024-04-21 19:22:01 +01:00
cargo_toml ::{ append_bins , bins_start_end_ind } ,
2024-04-17 14:55:50 +01:00
info_file ::{ ExerciseInfo , InfoFile } ,
2024-04-21 18:26:19 +01:00
CURRENT_FORMAT_VERSION , DEBUG_PROFILE ,
2024-04-17 14:55:50 +01:00
} ;
2024-04-16 02:30:28 +01:00
2024-04-17 17:19:08 +01:00
fn forbidden_char ( input : & str ) -> Option < char > {
input . chars ( ) . find ( | c | * c ! = '_' & & ! c . is_alphanumeric ( ) )
}
fn check_info_file_exercises ( info_file : & InfoFile ) -> Result < hashbrown ::HashSet < PathBuf > > {
let mut names = hashbrown ::HashSet ::with_capacity ( info_file . exercises . len ( ) ) ;
let mut paths = hashbrown ::HashSet ::with_capacity ( info_file . exercises . len ( ) ) ;
2024-04-17 17:59:40 +01:00
2024-04-17 21:46:21 +01:00
let mut file_buf = String ::with_capacity ( 1 < < 14 ) ;
2024-04-17 17:19:08 +01:00
for exercise_info in & info_file . exercises {
2024-04-17 18:12:10 +01:00
if exercise_info . name . is_empty ( ) {
bail! ( " Found an empty exercise name in `info.toml` " ) ;
}
2024-04-17 17:19:08 +01:00
if let Some ( c ) = forbidden_char ( & exercise_info . name ) {
bail! (
" Char `{c}` in the exercise name `{}` is not allowed " ,
exercise_info . name ,
) ;
}
if let Some ( dir ) = & exercise_info . dir {
2024-04-17 18:12:10 +01:00
if dir . is_empty ( ) {
2024-04-17 21:46:21 +01:00
bail! (
" The exercise `{}` has an empty dir name in `info.toml` " ,
exercise_info . name ,
) ;
2024-04-17 18:12:10 +01:00
}
2024-04-17 17:19:08 +01:00
if let Some ( c ) = forbidden_char ( dir ) {
bail! ( " Char `{c}` in the exercise dir `{dir}` is not allowed " ) ;
}
}
2024-04-17 18:16:48 +01:00
if exercise_info . hint . trim ( ) . is_empty ( ) {
2024-04-17 18:12:10 +01:00
bail! ( " The exercise `{}` has an empty hint. Please provide a hint or at least tell the user why a hint isn't needed for this exercise " , exercise_info . name ) ;
}
2024-04-17 17:19:08 +01:00
if ! names . insert ( exercise_info . name . as_str ( ) ) {
bail! (
2024-04-17 21:46:21 +01:00
" The exercise name `{}` is duplicated. Exercise names must all be unique " ,
2024-04-17 17:19:08 +01:00
exercise_info . name ,
) ;
}
2024-04-17 21:46:21 +01:00
let path = exercise_info . path ( ) ;
OpenOptions ::new ( )
. read ( true )
. open ( & path )
. with_context ( | | format! ( " Failed to open the file {path} " ) ) ?
. read_to_string ( & mut file_buf )
. with_context ( | | format! ( " Failed to read the file {path} " ) ) ? ;
if ! file_buf . contains ( " fn main() " ) {
bail! ( " The `main` function is missing in the file `{path}`. \n Create at least an empty `main` function to avoid language server errors " ) ;
}
file_buf . clear ( ) ;
paths . insert ( PathBuf ::from ( path ) ) ;
2024-04-17 17:19:08 +01:00
}
Ok ( paths )
}
2024-04-17 21:46:21 +01:00
fn unexpected_file ( path : & Path ) -> Error {
anyhow! ( " Found the file `{}`. Only `README.md` and Rust files related to an exercise in `info.toml` are allowed in the `exercises` directory " , path . display ( ) )
}
2024-04-17 17:59:40 +01:00
2024-04-18 10:40:54 +01:00
fn check_exercise_dir_files ( info_file_paths : & hashbrown ::HashSet < PathBuf > ) -> Result < ( ) > {
2024-04-17 17:19:08 +01:00
for entry in read_dir ( " exercises " ) . context ( " Failed to open the `exercises` directory " ) ? {
let entry = entry . context ( " Failed to read the `exercises` directory " ) ? ;
if entry . file_type ( ) . unwrap ( ) . is_file ( ) {
let path = entry . path ( ) ;
let file_name = path . file_name ( ) . unwrap ( ) ;
if file_name = = " README.md " {
continue ;
}
if ! info_file_paths . contains ( & path ) {
2024-04-17 21:46:21 +01:00
return Err ( unexpected_file ( & path ) ) ;
2024-04-17 17:19:08 +01:00
}
continue ;
}
let dir_path = entry . path ( ) ;
for entry in read_dir ( & dir_path )
. with_context ( | | format! ( " Failed to open the directory {} " , dir_path . display ( ) ) ) ?
{
let entry = entry
. with_context ( | | format! ( " Failed to read the directory {} " , dir_path . display ( ) ) ) ? ;
let path = entry . path ( ) ;
if ! entry . file_type ( ) . unwrap ( ) . is_file ( ) {
2024-04-17 21:46:21 +01:00
bail! ( " Found `{}` but expected only files. Only one level of exercise nesting is allowed " , path . display ( ) ) ;
2024-04-17 17:19:08 +01:00
}
let file_name = path . file_name ( ) . unwrap ( ) ;
if file_name = = " README.md " {
continue ;
}
if ! info_file_paths . contains ( & path ) {
2024-04-17 21:46:21 +01:00
return Err ( unexpected_file ( & path ) ) ;
2024-04-17 17:19:08 +01:00
}
}
}
2024-04-17 21:46:21 +01:00
Ok ( ( ) )
2024-04-17 17:19:08 +01:00
}
2024-04-17 21:46:21 +01:00
fn check_exercises ( info_file : & InfoFile ) -> Result < ( ) > {
2024-04-17 17:19:08 +01:00
match info_file . format_version . cmp ( & CURRENT_FORMAT_VERSION ) {
Ordering ::Less = > bail! ( " `format_version` < {CURRENT_FORMAT_VERSION} (supported version) \n Please migrate to the latest format version " ) ,
Ordering ::Greater = > bail! ( " `format_version` > {CURRENT_FORMAT_VERSION} (supported version) \n Try updating the Rustlings program " ) ,
Ordering ::Equal = > ( ) ,
}
let info_file_paths = check_info_file_exercises ( info_file ) ? ;
2024-04-18 10:40:54 +01:00
check_exercise_dir_files ( & info_file_paths ) ? ;
2024-04-17 17:19:08 +01:00
Ok ( ( ) )
}
2024-04-17 14:55:50 +01:00
fn check_cargo_toml (
exercise_infos : & [ ExerciseInfo ] ,
current_cargo_toml : & str ,
exercise_path_prefix : & [ u8 ] ,
) -> Result < ( ) > {
let ( bins_start_ind , bins_end_ind ) = bins_start_end_ind ( current_cargo_toml ) ? ;
let old_bins = & current_cargo_toml . as_bytes ( ) [ bins_start_ind .. bins_end_ind ] ;
let mut new_bins = Vec ::with_capacity ( 1 < < 13 ) ;
append_bins ( & mut new_bins , exercise_infos , exercise_path_prefix ) ;
if old_bins ! = new_bins {
2024-04-21 19:22:01 +01:00
if DEBUG_PROFILE {
bail! ( " The file `dev/Cargo.toml` is outdated. Please run `cargo run -- dev update` to update it " ) ;
}
2024-04-25 14:58:46 +01:00
bail! ( " The file `Cargo.toml` is outdated. Please run `rustlings dev update` to update it " ) ;
2024-04-17 14:55:50 +01:00
}
Ok ( ( ) )
}
pub fn check ( ) -> Result < ( ) > {
let info_file = InfoFile ::parse ( ) ? ;
2024-04-17 21:46:21 +01:00
check_exercises ( & info_file ) ? ;
2024-04-16 02:30:28 +01:00
2024-04-21 18:26:19 +01:00
if DEBUG_PROFILE {
2024-04-17 14:55:50 +01:00
check_cargo_toml (
& info_file . exercises ,
include_str! ( " ../../dev/Cargo.toml " ) ,
b " ../ " ,
2024-04-21 19:22:01 +01:00
) ? ;
2024-04-17 14:55:50 +01:00
} else {
let current_cargo_toml =
fs ::read_to_string ( " Cargo.toml " ) . context ( " Failed to read the file `Cargo.toml` " ) ? ;
2024-04-21 19:22:01 +01:00
check_cargo_toml ( & info_file . exercises , & current_cargo_toml , b " " ) ? ;
2024-04-17 14:55:50 +01:00
}
2024-04-16 02:30:28 +01:00
2024-04-16 02:35:23 +01:00
println! ( " \n Everything looks fine! " ) ;
2024-04-16 02:30:28 +01:00
Ok ( ( ) )
2024-04-15 22:54:57 +01:00
}