1
1
use anyhow:: { bail, Context , Result } ;
2
2
use std:: {
3
3
env,
4
- fs:: { self , File } ,
5
- io:: { self , Read , StdoutLock , Write } ,
4
+ fs:: { File , OpenOptions } ,
5
+ io:: { self , Read , Seek , StdoutLock , Write } ,
6
6
path:: Path ,
7
7
process:: { Command , Stdio } ,
8
8
thread,
@@ -18,7 +18,6 @@ use crate::{
18
18
} ;
19
19
20
20
const STATE_FILE_NAME : & str = ".rustlings-state.txt" ;
21
- const BAD_INDEX_ERR : & str = "The current exercise index is higher than the number of exercises" ;
22
21
23
22
#[ must_use]
24
23
pub enum ExercisesProgress {
@@ -47,6 +46,7 @@ pub struct AppState {
47
46
// Caches the number of done exercises to avoid iterating over all exercises every time.
48
47
n_done : u16 ,
49
48
final_message : String ,
49
+ state_file : File ,
50
50
// Preallocated buffer for reading and writing the state file.
51
51
file_buf : Vec < u8 > ,
52
52
official_exercises : bool ,
@@ -56,59 +56,22 @@ pub struct AppState {
56
56
}
57
57
58
58
impl AppState {
59
- // Update the app state from the state file.
60
- fn update_from_file ( & mut self ) -> StateFileStatus {
61
- self . file_buf . clear ( ) ;
62
- self . n_done = 0 ;
63
-
64
- if File :: open ( STATE_FILE_NAME )
65
- . and_then ( |mut file| file. read_to_end ( & mut self . file_buf ) )
66
- . is_err ( )
67
- {
68
- return StateFileStatus :: NotRead ;
69
- }
70
-
71
- // See `Self::write` for more information about the file format.
72
- let mut lines = self . file_buf . split ( |c| * c == b'\n' ) . skip ( 2 ) ;
73
-
74
- let Some ( current_exercise_name) = lines. next ( ) else {
75
- return StateFileStatus :: NotRead ;
76
- } ;
77
-
78
- if current_exercise_name. is_empty ( ) || lines. next ( ) . is_none ( ) {
79
- return StateFileStatus :: NotRead ;
80
- }
81
-
82
- let mut done_exercises = hash_set_with_capacity ( self . exercises . len ( ) ) ;
83
-
84
- for done_exerise_name in lines {
85
- if done_exerise_name. is_empty ( ) {
86
- break ;
87
- }
88
- done_exercises. insert ( done_exerise_name) ;
89
- }
90
-
91
- for ( ind, exercise) in self . exercises . iter_mut ( ) . enumerate ( ) {
92
- if done_exercises. contains ( exercise. name . as_bytes ( ) ) {
93
- exercise. done = true ;
94
- self . n_done += 1 ;
95
- }
96
-
97
- if exercise. name . as_bytes ( ) == current_exercise_name {
98
- self . current_exercise_ind = ind;
99
- }
100
- }
101
-
102
- StateFileStatus :: Read
103
- }
104
-
105
59
pub fn new (
106
60
exercise_infos : Vec < ExerciseInfo > ,
107
61
final_message : String ,
108
62
) -> Result < ( Self , StateFileStatus ) > {
109
63
let cmd_runner = CmdRunner :: build ( ) ?;
110
-
111
- let exercises = exercise_infos
64
+ let mut state_file = OpenOptions :: new ( )
65
+ . create ( true )
66
+ . read ( true )
67
+ . write ( true )
68
+ . truncate ( false )
69
+ . open ( STATE_FILE_NAME )
70
+ . with_context ( || {
71
+ format ! ( "Failed to open or create the state file {STATE_FILE_NAME}" )
72
+ } ) ?;
73
+
74
+ let mut exercises = exercise_infos
112
75
. into_iter ( )
113
76
. map ( |exercise_info| {
114
77
// Leaking to be able to borrow in the watch mode `Table`.
@@ -126,25 +89,69 @@ impl AppState {
126
89
test : exercise_info. test ,
127
90
strict_clippy : exercise_info. strict_clippy ,
128
91
hint,
129
- // Updated in `Self::update_from_file` .
92
+ // Updated below .
130
93
done : false ,
131
94
}
132
95
} )
133
96
. collect :: < Vec < _ > > ( ) ;
134
97
135
- let mut slf = Self {
136
- current_exercise_ind : 0 ,
98
+ let mut current_exercise_ind = 0 ;
99
+ let mut n_done = 0 ;
100
+ let mut file_buf = Vec :: with_capacity ( 2048 ) ;
101
+ let state_file_status = ' block: {
102
+ if state_file. read_to_end ( & mut file_buf) . is_err ( ) {
103
+ break ' block StateFileStatus :: NotRead ;
104
+ }
105
+
106
+ // See `Self::write` for more information about the file format.
107
+ let mut lines = file_buf. split ( |c| * c == b'\n' ) . skip ( 2 ) ;
108
+
109
+ let Some ( current_exercise_name) = lines. next ( ) else {
110
+ break ' block StateFileStatus :: NotRead ;
111
+ } ;
112
+
113
+ if current_exercise_name. is_empty ( ) || lines. next ( ) . is_none ( ) {
114
+ break ' block StateFileStatus :: NotRead ;
115
+ }
116
+
117
+ let mut done_exercises = hash_set_with_capacity ( exercises. len ( ) ) ;
118
+
119
+ for done_exerise_name in lines {
120
+ if done_exerise_name. is_empty ( ) {
121
+ break ;
122
+ }
123
+ done_exercises. insert ( done_exerise_name) ;
124
+ }
125
+
126
+ for ( ind, exercise) in exercises. iter_mut ( ) . enumerate ( ) {
127
+ if done_exercises. contains ( exercise. name . as_bytes ( ) ) {
128
+ exercise. done = true ;
129
+ n_done += 1 ;
130
+ }
131
+
132
+ if exercise. name . as_bytes ( ) == current_exercise_name {
133
+ current_exercise_ind = ind;
134
+ }
135
+ }
136
+
137
+ StateFileStatus :: Read
138
+ } ;
139
+
140
+ file_buf. clear ( ) ;
141
+ file_buf. extend_from_slice ( STATE_FILE_HEADER ) ;
142
+
143
+ let slf = Self {
144
+ current_exercise_ind,
137
145
exercises,
138
- n_done : 0 ,
146
+ n_done,
139
147
final_message,
140
- file_buf : Vec :: with_capacity ( 2048 ) ,
148
+ state_file,
149
+ file_buf,
141
150
official_exercises : !Path :: new ( "info.toml" ) . exists ( ) ,
142
151
cmd_runner,
143
152
vs_code : env:: var_os ( "TERM_PROGRAM" ) . is_some_and ( |v| v == "vscode" ) ,
144
153
} ;
145
154
146
- let state_file_status = slf. update_from_file ( ) ;
147
-
148
155
Ok ( ( slf, state_file_status) )
149
156
}
150
157
@@ -187,10 +194,8 @@ impl AppState {
187
194
// - The fourth line is an empty line.
188
195
// - All remaining lines are the names of done exercises.
189
196
fn write ( & mut self ) -> Result < ( ) > {
190
- self . file_buf . clear ( ) ;
197
+ self . file_buf . truncate ( STATE_FILE_HEADER . len ( ) ) ;
191
198
192
- self . file_buf
193
- . extend_from_slice ( b"DON'T EDIT THIS FILE!\n \n " ) ;
194
199
self . file_buf
195
200
. extend_from_slice ( self . current_exercise ( ) . name . as_bytes ( ) ) ;
196
201
self . file_buf . push ( b'\n' ) ;
@@ -202,7 +207,14 @@ impl AppState {
202
207
}
203
208
}
204
209
205
- fs:: write ( STATE_FILE_NAME , & self . file_buf )
210
+ self . state_file
211
+ . rewind ( )
212
+ . with_context ( || format ! ( "Failed to rewind the state file {STATE_FILE_NAME}" ) ) ?;
213
+ self . state_file
214
+ . set_len ( 0 )
215
+ . with_context ( || format ! ( "Failed to truncate the state file {STATE_FILE_NAME}" ) ) ?;
216
+ self . state_file
217
+ . write_all ( & self . file_buf )
206
218
. with_context ( || format ! ( "Failed to write the state file {STATE_FILE_NAME}" ) ) ?;
207
219
208
220
Ok ( ( ) )
@@ -440,11 +452,12 @@ impl AppState {
440
452
}
441
453
}
442
454
455
+ const BAD_INDEX_ERR : & str = "The current exercise index is higher than the number of exercises" ;
456
+ const STATE_FILE_HEADER : & [ u8 ] = b"DON'T EDIT THIS FILE!\n \n " ;
443
457
const RERUNNING_ALL_EXERCISES_MSG : & [ u8 ] = b"
444
458
All exercises seem to be done.
445
459
Recompiling and running all exercises to make sure that all of them are actually done.
446
460
" ;
447
-
448
461
const FENISH_LINE : & str = "+----------------------------------------------------+
449
462
| You made it to the Fe-nish line! |
450
463
+-------------------------- ------------------------+
@@ -490,6 +503,7 @@ mod tests {
490
503
exercises : vec ! [ dummy_exercise( ) , dummy_exercise( ) , dummy_exercise( ) ] ,
491
504
n_done : 0 ,
492
505
final_message : String :: new ( ) ,
506
+ state_file : tempfile:: tempfile ( ) . unwrap ( ) ,
493
507
file_buf : Vec :: new ( ) ,
494
508
official_exercises : true ,
495
509
cmd_runner : CmdRunner :: build ( ) . unwrap ( ) ,
0 commit comments