Borrow deserialized values

This commit is contained in:
mo8it 2026-02-26 15:55:07 +01:00
parent 13564207cb
commit 0cbcb8964c
10 changed files with 57 additions and 54 deletions

8
Cargo.lock generated
View File

@ -640,9 +640,9 @@ dependencies = [
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.9.12+spec-1.1.0" version = "1.0.3+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" checksum = "c7614eaf19ad818347db24addfa201729cf2a9b6fdfd9eb0ab870fcacc606c0c"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"serde_core", "serde_core",
@ -655,9 +655,9 @@ dependencies = [
[[package]] [[package]]
name = "toml_datetime" name = "toml_datetime"
version = "0.7.5+spec-1.1.0" version = "1.0.0+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e"
dependencies = [ dependencies = [
"serde_core", "serde_core",
] ]

View File

@ -19,7 +19,7 @@ rust-version = "1.88"
[workspace.dependencies] [workspace.dependencies]
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
toml = { version = "0.9", default-features = false, features = ["std", "parse", "serde"] } toml = { version = "1", default-features = false, features = ["std", "parse", "serde"] }
[package] [package]
name = "rustlings" name = "rustlings"

View File

@ -3,14 +3,15 @@ use quote::quote;
use serde::Deserialize; use serde::Deserialize;
#[derive(Deserialize)] #[derive(Deserialize)]
struct ExerciseInfo { struct ExerciseInfo<'a> {
name: String, name: &'a str,
dir: String, dir: &'a str,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
struct InfoFile { struct InfoFile<'a> {
exercises: Vec<ExerciseInfo>, #[serde(borrow)]
exercises: Vec<ExerciseInfo<'a>>,
} }
#[proc_macro] #[proc_macro]
@ -37,7 +38,7 @@ pub fn include_files(_: TokenStream) -> TokenStream {
continue; continue;
} }
dirs.push(exercise.dir.as_str()); dirs.push(exercise.dir);
*dir_ind = dirs.len() - 1; *dir_ind = dirs.len() - 1;
} }

View File

@ -54,7 +54,7 @@ pub struct AppState {
exercises: Vec<Exercise>, exercises: Vec<Exercise>,
// Caches the number of done exercises to avoid iterating over all exercises every time. // Caches the number of done exercises to avoid iterating over all exercises every time.
n_done: u16, n_done: u16,
final_message: String, final_message: &'static str,
state_file: File, state_file: File,
// Preallocated buffer for reading and writing the state file. // Preallocated buffer for reading and writing the state file.
file_buf: Vec<u8>, file_buf: Vec<u8>,
@ -66,7 +66,7 @@ pub struct AppState {
impl AppState { impl AppState {
pub fn new( pub fn new(
exercise_infos: Vec<ExerciseInfo>, exercise_infos: Vec<ExerciseInfo>,
final_message: String, final_message: &'static str,
) -> Result<(Self, StateFileStatus)> { ) -> Result<(Self, StateFileStatus)> {
let cmd_runner = CmdRunner::build()?; let cmd_runner = CmdRunner::build()?;
let mut state_file = OpenOptions::new() let mut state_file = OpenOptions::new()
@ -87,34 +87,33 @@ impl AppState {
// Leaking is not a problem because the `AppState` instance lives until // Leaking is not a problem because the `AppState` instance lives until
// the end of the program. // the end of the program.
let path = exercise_info.path().leak(); let path = exercise_info.path().leak();
let name = exercise_info.name.leak(); let hint = exercise_info.hint.trim_ascii();
let dir = exercise_info.dir.map(|dir| &*dir.leak());
let hint = exercise_info.hint.leak().trim_ascii();
let canonical_path = dir_canonical_path.as_deref().map(|dir_canonical_path| { let canonical_path = dir_canonical_path.as_deref().map(|dir_canonical_path| {
let mut canonical_path; let mut canonical_path;
if let Some(dir) = dir { if let Some(dir) = exercise_info.dir {
canonical_path = String::with_capacity( canonical_path = String::with_capacity(
2 + dir_canonical_path.len() + dir.len() + name.len(), 2 + dir_canonical_path.len() + dir.len() + exercise_info.name.len(),
); );
canonical_path.push_str(dir_canonical_path); canonical_path.push_str(dir_canonical_path);
canonical_path.push_str(MAIN_SEPARATOR_STR); canonical_path.push_str(MAIN_SEPARATOR_STR);
canonical_path.push_str(dir); canonical_path.push_str(dir);
} else { } else {
canonical_path = canonical_path = String::with_capacity(
String::with_capacity(1 + dir_canonical_path.len() + name.len()); 1 + dir_canonical_path.len() + exercise_info.name.len(),
);
canonical_path.push_str(dir_canonical_path); canonical_path.push_str(dir_canonical_path);
} }
canonical_path.push_str(MAIN_SEPARATOR_STR); canonical_path.push_str(MAIN_SEPARATOR_STR);
canonical_path.push_str(name); canonical_path.push_str(exercise_info.name);
canonical_path.push_str(".rs"); canonical_path.push_str(".rs");
canonical_path canonical_path
}); });
Exercise { Exercise {
dir, dir: exercise_info.dir,
name, name: exercise_info.name,
path, path,
canonical_path, canonical_path,
test: exercise_info.test, test: exercise_info.test,
@ -616,7 +615,7 @@ mod tests {
current_exercise_ind: 0, current_exercise_ind: 0,
exercises: vec![dummy_exercise(), dummy_exercise(), dummy_exercise()], exercises: vec![dummy_exercise(), dummy_exercise(), dummy_exercise()],
n_done: 0, n_done: 0,
final_message: String::new(), final_message: "",
state_file: tempfile::tempfile().unwrap(), state_file: tempfile::tempfile().unwrap(),
file_buf: Vec::new(), file_buf: Vec::new(),
official_exercises: true, official_exercises: true,

View File

@ -38,7 +38,7 @@ pub fn append_bins(
buf.extend_from_slice(b"\", path = \""); buf.extend_from_slice(b"\", path = \"");
buf.extend_from_slice(exercise_path_prefix); buf.extend_from_slice(exercise_path_prefix);
buf.extend_from_slice(b"exercises/"); buf.extend_from_slice(b"exercises/");
if let Some(dir) = &exercise_info.dir { if let Some(dir) = exercise_info.dir {
buf.extend_from_slice(dir.as_bytes()); buf.extend_from_slice(dir.as_bytes());
buf.push(b'/'); buf.push(b'/');
} }
@ -56,7 +56,7 @@ pub fn append_bins(
buf.extend_from_slice(b"\", path = \""); buf.extend_from_slice(b"\", path = \"");
buf.extend_from_slice(exercise_path_prefix); buf.extend_from_slice(exercise_path_prefix);
buf.extend_from_slice(b"solutions/"); buf.extend_from_slice(b"solutions/");
if let Some(dir) = &exercise_info.dir { if let Some(dir) = exercise_info.dir {
buf.extend_from_slice(dir.as_bytes()); buf.extend_from_slice(dir.as_bytes());
buf.push(b'/'); buf.push(b'/');
} }
@ -106,19 +106,19 @@ mod tests {
fn test_bins() { fn test_bins() {
let exercise_infos = [ let exercise_infos = [
ExerciseInfo { ExerciseInfo {
name: String::from("1"), name: "1",
dir: None, dir: None,
test: true, test: true,
strict_clippy: true, strict_clippy: true,
hint: String::new(), hint: "",
skip_check_unsolved: false, skip_check_unsolved: false,
}, },
ExerciseInfo { ExerciseInfo {
name: String::from("2"), name: "2",
dir: Some(String::from("d")), dir: Some("d"),
test: false, test: false,
strict_clippy: false, strict_clippy: false,
hint: String::new(), hint: "",
skip_check_unsolved: false, skip_check_unsolved: false,
}, },
]; ];

View File

@ -63,7 +63,7 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result<HashSet<PathBuf>> {
let mut file_buf = String::with_capacity(1 << 14); let mut file_buf = String::with_capacity(1 << 14);
for exercise_info in &info_file.exercises { for exercise_info in &info_file.exercises {
let name = exercise_info.name.as_str(); let name = exercise_info.name;
if name.is_empty() { if name.is_empty() {
bail!("Found an empty exercise name in `info.toml`"); bail!("Found an empty exercise name in `info.toml`");
} }
@ -76,7 +76,7 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result<HashSet<PathBuf>> {
bail!("Char `{c}` in the exercise name `{name}` is not allowed"); bail!("Char `{c}` in the exercise name `{name}` is not allowed");
} }
if let Some(dir) = &exercise_info.dir { if let Some(dir) = exercise_info.dir {
if dir.is_empty() { if dir.is_empty() {
bail!("The exercise `{name}` has an empty dir name in `info.toml`"); bail!("The exercise `{name}` has an empty dir name in `info.toml`");
} }
@ -214,7 +214,7 @@ fn check_exercises_unsolved(
Some( Some(
thread::Builder::new() thread::Builder::new()
.spawn(|| exercise_info.run_exercise(None, cmd_runner)) .spawn(|| exercise_info.run_exercise(None, cmd_runner))
.map(|handle| (exercise_info.name.as_str(), handle)), .map(|handle| (exercise_info.name, handle)),
) )
}) })
.collect::<Result<Vec<_>, _>>() .collect::<Result<Vec<_>, _>>()

View File

@ -85,7 +85,7 @@ impl EmbeddedFiles {
exercise_path.truncate(prefix.len()); exercise_path.truncate(prefix.len());
exercise_path.push_str(dir.name); exercise_path.push_str(dir.name);
exercise_path.push('/'); exercise_path.push('/');
exercise_path.push_str(&exercise_info.name); exercise_path.push_str(exercise_info.name);
exercise_path.push_str(".rs"); exercise_path.push_str(".rs");
fs::write(&exercise_path, exercise_files.exercise) fs::write(&exercise_path, exercise_files.exercise)
@ -141,13 +141,14 @@ mod tests {
use super::*; use super::*;
#[derive(Deserialize)] #[derive(Deserialize)]
struct ExerciseInfo { struct ExerciseInfo<'a> {
dir: String, dir: &'a str,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
struct InfoFile { struct InfoFile<'a> {
exercises: Vec<ExerciseInfo>, #[serde(borrow)]
exercises: Vec<ExerciseInfo<'a>>,
} }
#[test] #[test]

View File

@ -8,9 +8,9 @@ use crate::{embedded::EMBEDDED_FILES, exercise::RunnableExercise};
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct ExerciseInfo { pub struct ExerciseInfo {
/// Exercise's unique name. /// Exercise's unique name.
pub name: String, pub name: &'static str,
/// Exercise's directory name inside the `exercises/` directory. /// Exercise's directory name inside the `exercises/` directory.
pub dir: Option<String>, pub dir: Option<&'static str>,
/// Run `cargo test` on the exercise. /// Run `cargo test` on the exercise.
#[serde(default = "default_true")] #[serde(default = "default_true")]
pub test: bool, pub test: bool,
@ -18,7 +18,7 @@ pub struct ExerciseInfo {
#[serde(default)] #[serde(default)]
pub strict_clippy: bool, pub strict_clippy: bool,
/// The exercise's hint to be shown to the user on request. /// The exercise's hint to be shown to the user on request.
pub hint: String, pub hint: &'static str,
/// The exercise is already solved. Ignore it when checking that all exercises are unsolved. /// The exercise is already solved. Ignore it when checking that all exercises are unsolved.
#[serde(default)] #[serde(default)]
pub skip_check_unsolved: bool, pub skip_check_unsolved: bool,
@ -31,7 +31,7 @@ const fn default_true() -> bool {
impl ExerciseInfo { impl ExerciseInfo {
/// Path to the exercise file starting with the `exercises/` directory. /// Path to the exercise file starting with the `exercises/` directory.
pub fn path(&self) -> String { pub fn path(&self) -> String {
let mut path = if let Some(dir) = &self.dir { let mut path = if let Some(dir) = self.dir {
// 14 = 10 + 1 + 3 // 14 = 10 + 1 + 3
// exercises/ + / + .rs // exercises/ + / + .rs
let mut path = String::with_capacity(14 + dir.len() + self.name.len()); let mut path = String::with_capacity(14 + dir.len() + self.name.len());
@ -47,7 +47,7 @@ impl ExerciseInfo {
path path
}; };
path.push_str(&self.name); path.push_str(self.name);
path.push_str(".rs"); path.push_str(".rs");
path path
@ -57,12 +57,12 @@ impl ExerciseInfo {
impl RunnableExercise for ExerciseInfo { impl RunnableExercise for ExerciseInfo {
#[inline] #[inline]
fn name(&self) -> &str { fn name(&self) -> &str {
&self.name self.name
} }
#[inline] #[inline]
fn dir(&self) -> Option<&str> { fn dir(&self) -> Option<&str> {
self.dir.as_deref() self.dir
} }
#[inline] #[inline]
@ -82,9 +82,9 @@ pub struct InfoFile {
/// For possible breaking changes in the future for community exercises. /// For possible breaking changes in the future for community exercises.
pub format_version: u8, pub format_version: u8,
/// Shown to users when starting with the exercises. /// Shown to users when starting with the exercises.
pub welcome_message: Option<String>, pub welcome_message: Option<&'static str>,
/// Shown to users after finishing all exercises. /// Shown to users after finishing all exercises.
pub final_message: Option<String>, pub final_message: Option<&'static str>,
/// List of all exercises. /// List of all exercises.
pub exercises: Vec<ExerciseInfo>, pub exercises: Vec<ExerciseInfo>,
} }
@ -95,7 +95,8 @@ impl InfoFile {
pub fn parse() -> Result<Self> { pub fn parse() -> Result<Self> {
// Read a local `info.toml` if it exists. // Read a local `info.toml` if it exists.
let slf = match fs::read_to_string("info.toml") { let slf = match fs::read_to_string("info.toml") {
Ok(file_content) => toml::de::from_str::<Self>(&file_content) // Leaking is fine since `InfoFile` is used until the end of the program.
Ok(file_content) => toml::de::from_str::<Self>(file_content.leak())
.context("Failed to parse the `info.toml` file")?, .context("Failed to parse the `info.toml` file")?,
Err(e) => { Err(e) => {
if e.kind() == ErrorKind::NotFound { if e.kind() == ErrorKind::NotFound {

View File

@ -8,7 +8,7 @@ use std::{
env::set_current_dir, env::set_current_dir,
fs::{self, create_dir}, fs::{self, create_dir},
io::{self, Write}, io::{self, Write},
path::{Path, PathBuf}, path::Path,
process::{Command, Stdio}, process::{Command, Stdio},
}; };
@ -18,8 +18,9 @@ use crate::{
}; };
#[derive(Deserialize)] #[derive(Deserialize)]
struct CargoLocateProject { struct CargoLocateProject<'a> {
root: PathBuf, #[serde(borrow)]
root: &'a Path,
} }
pub fn init() -> Result<()> { pub fn init() -> Result<()> {
@ -72,7 +73,7 @@ pub fn init() -> Result<()> {
)? )?
.root; .root;
let workspace_manifest_content = fs::read_to_string(&workspace_manifest) let workspace_manifest_content = fs::read_to_string(workspace_manifest)
.with_context(|| format!("Failed to read the file {}", workspace_manifest.display()))?; .with_context(|| format!("Failed to read the file {}", workspace_manifest.display()))?;
if !workspace_manifest_content.contains("[workspace]") if !workspace_manifest_content.contains("[workspace]")
&& !workspace_manifest_content.contains("workspace.") && !workspace_manifest_content.contains("workspace.")

View File

@ -128,7 +128,7 @@ fn main() -> Result<ExitCode> {
None None
} else { } else {
// For the notify event handler thread. // For the notify event handler thread.
// Leaking is not a problem because the slice lives until the end of the program. // Leaking is fine since the slice is used until the end of the program.
Some( Some(
&*app_state &*app_state
.exercises() .exercises()