├── .gitignore ├── sio2jail ├── .idea ├── vcs.xml ├── .gitignore ├── misc.xml └── modules.xml ├── src ├── generic_utils.rs ├── formatted_error.rs ├── temp_files.rs ├── prepare_input.rs ├── executor │ ├── mod.rs │ ├── simple.rs │ └── sio2jail.rs ├── testing_utils.rs ├── test_errors.rs ├── checker.rs ├── compiler.rs ├── test_summary.rs ├── args.rs └── main.rs ├── toster.iml ├── Cargo.toml ├── LICENSE ├── README.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | in/* 3 | out/* -------------------------------------------------------------------------------- /sio2jail: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikolajKolek/toster/HEAD/sio2jail -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | /discord.xml 10 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/generic_utils.rs: -------------------------------------------------------------------------------- 1 | use std::thread; 2 | use std::time::Duration; 3 | 4 | #[deprecated(note = "This is not ideal, there must be a better way to implement it")] 5 | pub(crate) fn halt() -> ! { 6 | thread::sleep(Duration::from_secs(u64::MAX)); 7 | unreachable!() 8 | } -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /toster.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/formatted_error.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter}; 2 | use colored::Colorize; 3 | 4 | pub(crate) struct FormattedError(String); 5 | 6 | impl FormattedError { 7 | pub(crate) fn preformatted(formatted_string: String) -> FormattedError { 8 | FormattedError(formatted_string) 9 | } 10 | 11 | pub(crate) fn from_str(string: &str) -> FormattedError { 12 | FormattedError(string.red().to_string()) 13 | } 14 | } 15 | 16 | impl Display for FormattedError { 17 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 18 | f.write_str(&self.0) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "toster" 3 | description = "A simple-as-toast tester for C++ solutions to competitive programming exercises" 4 | repository = "https://github.com/MikolajKolek/toster" 5 | homepage = "https://github.com/MikolajKolek/toster" 6 | authors = ["Mikołaj Kołek", "Dominik Korsa"] 7 | readme = "README.md" 8 | license = "MIT" 9 | version = "1.2.1" 10 | edition = "2021" 11 | build = "build.rs" 12 | 13 | [dependencies] 14 | clap = { version = "4.5.4", features = ["derive"] } 15 | indicatif = { version = "0.17.8", features = ["rayon"] } 16 | rayon = "1.10.0" 17 | colored = "2.1.0" 18 | wait-timeout = "0.2.0" 19 | comfy-table = "7.1.1" 20 | tempfile = "3.10.1" 21 | terminal_size = "0.3.0" 22 | human-sort = "0.2.2" 23 | human-panic = "2.0.0" 24 | is_executable = "1.0.1" 25 | ctrlc = "3.4.4" 26 | directories = "5.0.1" 27 | which = "6.0.1" 28 | 29 | [target.'cfg(all(target_os = "linux", target_arch = "x86_64"))'.dependencies] 30 | command-fds = "0.3.0" 31 | 32 | [target.'cfg(target_os = "linux")'.dependencies] 33 | memfile = "0.3.2" 34 | 35 | [build-dependencies] 36 | directories = "5.0.1" -------------------------------------------------------------------------------- /src/temp_files.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io; 3 | use std::process::Stdio; 4 | 5 | pub(crate) fn make_cloned_stdio(file: &File) -> Stdio { 6 | Stdio::from(file.try_clone().unwrap()) 7 | } 8 | 9 | /// Creates a memfile using the `memfile` crate on Linux 10 | /// or a tempfile using the `tempfile` crate on other systems. 11 | /// 12 | /// These files should be deleted automatically when all file descriptors are closed 13 | /// 14 | /// Always returns a `File` struct 15 | pub(crate) fn create_temp_file() -> io::Result { 16 | #[cfg(target_os = "linux")] 17 | { 18 | // The file is deleted when all file descriptors are closed 19 | // https://man7.org/linux/man-pages/man2/memfd_create.2.html 20 | memfile::MemFile::create_default("toster temporary file") 21 | .map(|memfile| memfile.into_file()) 22 | } 23 | 24 | #[cfg(not(target_os = "linux"))] 25 | { 26 | // tempfile() adds FILE_FLAG_DELETE_ON_CLOSE flag on Windows and TMPFILE on Unix 27 | // so the file should be deleted when all file descriptors are closed 28 | tempfile::tempfile() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-2024 Mikołaj Kołek 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/prepare_input.rs: -------------------------------------------------------------------------------- 1 | use std::fs::{File, read_dir}; 2 | use std::path::{Path, PathBuf}; 3 | use rayon::iter::{IndexedParallelIterator, IntoParallelIterator}; 4 | use rayon::vec::IntoIter; 5 | use crate::formatted_error::FormattedError; 6 | 7 | pub(crate) enum TestInputSource { 8 | File(PathBuf) 9 | } 10 | 11 | impl TestInputSource { 12 | pub(crate) fn get_file(&self) -> File { 13 | match self { 14 | TestInputSource::File(path) => { File::open(path).expect("Failed to open input file") }, 15 | } 16 | } 17 | } 18 | 19 | pub(crate) struct Test { 20 | pub(crate) test_name: String, 21 | pub(crate) input_source: TestInputSource, 22 | } 23 | 24 | pub(crate) struct TestingInputs> { 25 | pub(crate) test_count: usize, 26 | pub(crate) iterator: T, 27 | } 28 | 29 | pub(crate) fn prepare_file_inputs(input_dir: &Path, in_ext: &str) -> Result>, FormattedError> { 30 | let tests: Vec = read_dir(input_dir) 31 | .expect("Cannot open input directory") 32 | .map(|input| { 33 | input.expect("Failed to read contents of input directory").path() 34 | }) 35 | .filter(|path| { 36 | return match path.extension() { 37 | None => false, 38 | Some(ext) => ".".to_owned() + ext.to_str().unwrap_or("") == in_ext 39 | }; 40 | }) 41 | .map(|file_path| { 42 | let test_name = file_path.file_stem().unwrap_or_else(|| panic!("The input file {} is invalid", file_path.display())).to_str().unwrap_or_else(|| panic!("The input file {} is invalid", file_path.display())).to_string(); 43 | Test { 44 | test_name, 45 | input_source: TestInputSource::File(file_path) 46 | } 47 | }) 48 | .collect(); 49 | 50 | if tests.is_empty() { 51 | return Err(FormattedError::from_str("There are no files in the input directory with the provided file extension")); 52 | } 53 | 54 | let test_count = tests.len(); 55 | 56 | Ok(TestingInputs { test_count, iterator: tests.into_par_iter() }) 57 | } -------------------------------------------------------------------------------- /src/executor/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod simple; 2 | #[cfg(all(target_os = "linux", target_arch = "x86_64"))] 3 | pub(crate) mod sio2jail; 4 | 5 | use std::fs::File; 6 | use std::io::{Read, Seek}; 7 | use crate::executor::simple::SimpleExecutor; 8 | #[cfg(all(target_os = "linux", target_arch = "x86_64"))] 9 | use crate::executor::sio2jail::Sio2jailExecutor; 10 | use crate::temp_files::create_temp_file; 11 | use crate::test_errors::{ExecutionError, ExecutionMetrics}; 12 | 13 | pub(crate) trait TestExecutor: Sync + Send { 14 | /// Executes the program. 15 | /// 16 | /// Stdin is read from `input_file`, stderr is ignored. 17 | /// Stdout is written to `output_file`. 18 | /// `input_file` might not be read fully. `output_file` **is not** rewound. 19 | fn test_to_file(&self, input_file: &File, output_file: &File) -> (ExecutionMetrics, Result<(), ExecutionError>); 20 | } 21 | 22 | /// Creates a tempfile for stdout and executes the program. 23 | /// 24 | /// Returns execution metrics and output file (if there are no errors during execution). 25 | /// 26 | /// Stdin is read from `input_file`, stderr is ignored. 27 | /// `input_file` might not be read fully. Output file **is** rewound before returning. 28 | pub(crate) fn test_to_temp(executor: &impl TestExecutor, input_file: &File) -> (ExecutionMetrics, Result) { 29 | let mut stdout_memfile = create_temp_file().expect("Failed to create memfile"); 30 | let (metrics, result) = executor.test_to_file( 31 | input_file, 32 | &stdout_memfile, 33 | ); 34 | stdout_memfile.rewind().expect("Failed to rewind memfile"); 35 | (metrics, result.map(|_| stdout_memfile)) 36 | } 37 | 38 | pub(crate) enum AnyTestExecutor { 39 | Simple(SimpleExecutor), 40 | #[cfg(all(target_os = "linux", target_arch = "x86_64"))] 41 | Sio2Jail(Sio2jailExecutor), 42 | } 43 | 44 | impl TestExecutor for AnyTestExecutor { 45 | fn test_to_file(&self, input_file: &File, output_file: &File) -> (ExecutionMetrics, Result<(), ExecutionError>) { 46 | match self { 47 | AnyTestExecutor::Simple(executor) => executor.test_to_file(input_file, output_file), 48 | #[cfg(all(target_os = "linux", target_arch = "x86_64"))] 49 | AnyTestExecutor::Sio2Jail(executor) => executor.test_to_file(input_file, output_file), 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/testing_utils.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::max; 2 | use std::fs; 3 | use std::io::{Read, read_to_string}; 4 | use std::path::Path; 5 | use comfy_table::{Attribute, Cell, Color, Table}; 6 | use comfy_table::ContentArrangement::Dynamic; 7 | use terminal_size::{Height, Width}; 8 | use crate::test_errors::TestError; 9 | use crate::test_errors::TestError::{Incorrect, NoOutputFile}; 10 | 11 | pub(crate) fn compare_output(expected_output_path: &Path, actual_output: impl Read) -> Result<(), TestError> { 12 | if !expected_output_path.is_file() { 13 | return Err(NoOutputFile); 14 | } 15 | let expected_output = fs::read_to_string(expected_output_path).expect("Failed to read output file"); 16 | let actual_output = read_to_string(actual_output).expect("Failed to read actual input"); 17 | 18 | let expected_output = split_trim_end(&expected_output); 19 | let actual_output = split_trim_end(&actual_output); 20 | 21 | if actual_output != expected_output { 22 | return Err(Incorrect { error: generate_diff(&expected_output, &actual_output) }); 23 | } 24 | Ok(()) 25 | } 26 | 27 | fn split_trim_end(to_split: &str) -> Vec<&str> { 28 | let mut res = to_split 29 | .split('\n') 30 | .map(|line| line.trim_end()) 31 | .collect::>(); 32 | 33 | while res.last().is_some_and(|last| last.trim().is_empty()) { 34 | res.pop(); 35 | } 36 | 37 | res 38 | } 39 | 40 | fn generate_diff(expected_split: &[&str], actual_split: &[&str]) -> String { 41 | let (Width(w), Height(_)) = terminal_size::terminal_size().unwrap_or((Width(40), Height(0))); 42 | let mut table = Table::new(); 43 | table.set_content_arrangement(Dynamic).set_width(w).set_header(vec![ 44 | Cell::new("Line").add_attribute(Attribute::Bold), 45 | Cell::new("Output file").add_attribute(Attribute::Bold).fg(Color::Green), 46 | Cell::new("Your program's output").add_attribute(Attribute::Bold).fg(Color::Red) 47 | ]); 48 | 49 | let mut row_count = 0; 50 | for i in 0..max(expected_split.len(), actual_split.len()) { 51 | let expected_line = expected_split.get(i).unwrap_or(&""); 52 | let actual_line = actual_split.get(i).unwrap_or(&""); 53 | 54 | if expected_line != actual_line { 55 | table.add_row(vec![ 56 | Cell::new(i + 1), 57 | Cell::new(expected_line).fg(Color::Green), 58 | Cell::new(actual_line).fg(Color::Red) 59 | ]); 60 | 61 | row_count += 1; 62 | } 63 | 64 | if row_count >= 99 { 65 | table.add_row(vec![ 66 | Cell::new("..."), 67 | Cell::new("..."), 68 | Cell::new("...") 69 | ]); 70 | 71 | break; 72 | } 73 | } 74 | 75 | table.to_string().replace('\r', "") 76 | } 77 | -------------------------------------------------------------------------------- /src/executor/simple.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::path::PathBuf; 3 | use std::process::{Child, Command, ExitStatus, Stdio}; 4 | use std::time::{Duration, Instant}; 5 | use crate::test_errors::{ExecutionError, ExecutionMetrics}; 6 | use wait_timeout::ChildExt; 7 | use crate::executor::TestExecutor; 8 | use crate::test_errors::ExecutionError::{RuntimeError, TimedOut}; 9 | 10 | #[cfg(unix)] 11 | use crate::generic_utils::halt; 12 | #[cfg(unix)] 13 | use std::os::unix::process::ExitStatusExt; 14 | use crate::temp_files::make_cloned_stdio; 15 | 16 | pub(crate) struct SimpleExecutor { 17 | pub(crate) timeout: Duration, 18 | pub(crate) executable_path: PathBuf, 19 | } 20 | 21 | impl SimpleExecutor { 22 | fn map_status_code(status: &ExitStatus) -> Result<(), ExecutionError> { 23 | match status.code() { 24 | Some(0) => Ok(()), 25 | Some(exit_code) => { 26 | Err(RuntimeError(format!("- the program returned a non-zero return code: {}", exit_code))) 27 | }, 28 | None => { 29 | #[cfg(unix)] 30 | if status.signal().expect("The program returned an invalid status code") == 2 { 31 | halt(); 32 | } 33 | 34 | Err(RuntimeError(format!("- the process was terminated with the following error:\n{}", status))) 35 | } 36 | } 37 | } 38 | 39 | fn wait_for_child(&self, mut child: Child) -> (ExecutionMetrics, Result<(), ExecutionError>) { 40 | let start_time = Instant::now(); 41 | let status = child.wait_timeout(self.timeout).unwrap(); 42 | 43 | match status { 44 | Some(status) => ( 45 | ExecutionMetrics { time: Some(start_time.elapsed()), memory_kibibytes: None }, 46 | SimpleExecutor::map_status_code(&status) 47 | ), 48 | None => { 49 | child.kill().unwrap(); 50 | (ExecutionMetrics { time: Some(self.timeout), memory_kibibytes: None }, Err(TimedOut)) 51 | } 52 | } 53 | } 54 | } 55 | 56 | impl TestExecutor for SimpleExecutor { 57 | fn test_to_file(&self, input_file: &File, output_file: &File) -> (ExecutionMetrics, Result<(), ExecutionError>) { 58 | let child = Command::new(&self.executable_path) 59 | .stdin(make_cloned_stdio(input_file)) 60 | .stdout(make_cloned_stdio(output_file)) 61 | .stderr(Stdio::null()) 62 | .spawn().expect("Failed to spawn child"); 63 | 64 | self.wait_for_child(child) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/test_errors.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | use colored::Colorize; 3 | 4 | pub struct ExecutionMetrics { 5 | pub(crate) memory_kibibytes: Option, 6 | pub(crate) time: Option, 7 | } 8 | 9 | impl ExecutionMetrics { 10 | // Currently only the sio2jail executor uses this constant, 11 | // which is not compiled on Windows builds 12 | #[allow(dead_code)] 13 | pub const NONE: ExecutionMetrics = ExecutionMetrics { memory_kibibytes: None, time: None }; 14 | } 15 | 16 | pub enum TestError { 17 | Incorrect { 18 | error: String 19 | }, 20 | ProgramError { 21 | error: ExecutionError 22 | }, 23 | CheckerError { 24 | error: ExecutionError 25 | }, 26 | NoOutputFile, 27 | Cancelled, 28 | } 29 | 30 | #[allow(unused)] 31 | #[derive(Debug)] 32 | pub enum ExecutionError { 33 | TimedOut, 34 | MemoryLimitExceeded, 35 | RuntimeError(String), 36 | Sio2jailError(String), 37 | PipeError, 38 | OutputNotUtf8, 39 | IncorrectCheckerFormat(String) 40 | } 41 | 42 | impl TestError { 43 | pub fn to_string(&self, test_name: &str) -> String { 44 | let mut result: String = String::new(); 45 | 46 | match self { 47 | TestError::Incorrect { error } => { 48 | result.push_str(&format!("{}", format!("Test {}:\n", test_name).bold())); 49 | result.push_str(error); 50 | } 51 | TestError::ProgramError { error } => { 52 | result.push_str(&format!("{}", format!("Test {}:\n", test_name).bold())); 53 | result.push_str(&format!("{}", error.to_string().red())); 54 | } 55 | TestError::CheckerError { error } => { 56 | result.push_str(&format!("{}", format!("Test {} encountered a checker error:\n", test_name).bold())); 57 | result.push_str(&format!("{}", error.to_string().blue())); 58 | } 59 | TestError::NoOutputFile => { 60 | result.push_str(&format!("{}", format!("Test {}:\n", test_name).bold())); 61 | result.push_str(&format!("{}", "Output file does not exist".red())); 62 | } 63 | TestError::Cancelled => { 64 | result.push_str(&format!("{}", format!("Test {}:\n", test_name).bold())); 65 | result.push_str(&format!("{}", "Cancelled".yellow())); 66 | } 67 | } 68 | 69 | result 70 | } 71 | } 72 | 73 | impl ExecutionError { 74 | pub fn to_string(&self) -> String { 75 | match self { 76 | ExecutionError::TimedOut => "Timed out".to_string(), 77 | ExecutionError::MemoryLimitExceeded => "Memory limit exceeded".to_string(), 78 | ExecutionError::RuntimeError(error) => format!("Runtime error {}", error), 79 | ExecutionError::Sio2jailError(error) => format!("Sio2jail error: {}", error), 80 | ExecutionError::IncorrectCheckerFormat(error) => format!("The checker output didn't follow the Toster checker format - {}", error), 81 | ExecutionError::PipeError => "Failed to read program output".to_string(), 82 | ExecutionError::OutputNotUtf8 => "The output contained invalid characters".to_string(), 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/checker.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::{read_to_string, Seek, Write}; 3 | use std::path::PathBuf; 4 | use std::io; 5 | use std::time::Duration; 6 | use colored::Colorize; 7 | use crate::executor::simple::SimpleExecutor; 8 | use crate::executor::test_to_temp; 9 | use crate::prepare_input::TestInputSource; 10 | use crate::temp_files::create_temp_file; 11 | use crate::test_errors::TestError; 12 | use crate::test_errors::ExecutionError::IncorrectCheckerFormat; 13 | use crate::test_errors::TestError::CheckerError; 14 | 15 | pub(crate) struct Checker { 16 | executor: SimpleExecutor 17 | } 18 | 19 | impl Checker { 20 | pub(crate) fn new(checker_executable: PathBuf, timeout: Duration) -> Self { 21 | Checker { 22 | executor: SimpleExecutor { 23 | executable_path: checker_executable, 24 | timeout, 25 | } 26 | } 27 | } 28 | 29 | fn parse_checker_output(output: &str) -> Result<(), TestError> { 30 | match output.chars().nth(0) { 31 | None => Err(CheckerError { error: IncorrectCheckerFormat("the checker returned an empty file".to_string()) }), 32 | Some('C') => Ok(()), 33 | Some('I') => { 34 | let checker_error = if output.len() > 1 { output.split_at(2).1.to_string() } else { String::new() }; 35 | let error_message = format!("Incorrect output{}{}", if checker_error.trim().is_empty() { "" } else { ": " }, checker_error.trim()).red(); 36 | Err(TestError::Incorrect { 37 | error: error_message.to_string(), 38 | }) 39 | } 40 | Some(_) => Err(CheckerError { error: IncorrectCheckerFormat("the first character of the checker's output wasn't C or I".to_string()) }) 41 | } 42 | } 43 | 44 | /// Creates a new temporary file for the checker input and writes the program input to it. 45 | /// The cursor is left at the end (not rewound). 46 | /// 47 | /// The program output should be appended to this file before calling check() on it, 48 | /// which can be done by passing the file as stdin to the tested program. 49 | pub(crate) fn prepare_checker_input(input_source: &TestInputSource) -> File { 50 | let mut input_memfile = create_temp_file().unwrap(); 51 | io::copy(&mut input_source.get_file(), &mut input_memfile).unwrap(); 52 | input_memfile.write_all("\n".as_bytes()).unwrap(); 53 | input_memfile 54 | } 55 | 56 | /// Run checker on input file created using `prepare_checker_input()`. 57 | /// The program output should be appended to that file. 58 | /// `check()` will rewind `checker_input` before running checker. 59 | pub(crate) fn check(&self, mut checker_input: File) -> Result<(), TestError> { 60 | checker_input.rewind().unwrap(); 61 | 62 | let (_, result) = test_to_temp(&self.executor, &checker_input); 63 | let output = match result { 64 | Ok(output) => output, 65 | Err(error) => { 66 | return Err(CheckerError { error }); 67 | } 68 | }; 69 | let output = read_to_string(output).expect("Failed to read checker output"); 70 | Self::parse_checker_output(&output) 71 | } 72 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Toster 2 | [![Crates.io](https://img.shields.io/crates/l/toster)](https://github.com/MikolajKolek/toster/blob/master/LICENSE) 3 | [![Crates.io](https://img.shields.io/crates/d/toster)](https://crates.io/crates/toster) 4 | [![Crates.io](https://img.shields.io/crates/v/toster)](https://crates.io/crates/toster) 5 | 6 | A simple-as-toast tester for C++ solutions to competitive programming exercises 7 | 8 | # Usage 9 | 10 | ``` 11 | Usage: toster [OPTIONS] 12 | 13 | Arguments: 14 | The name of the file containing the source code or the executable you want to test 15 | 16 | Options: 17 | -i, --in 18 | Input directory [default: in] 19 | --in-ext 20 | Input file extension [default: .in] 21 | -o, --out 22 | Output directory [default: out] 23 | --out-ext 24 | Output file extension [default: .out] 25 | --io 26 | The input and output directory (sets both -i and -o at once) 27 | -c, --checker 28 | The C++ source code or executable of a checker program that verifies if the tested program's output is correct instead of comparing it with given output files 29 | The checker must use the following protocol: 30 | - The checker receives the contents of the input file and the output of the tested program on stdin, separated by a single "\n" character 31 | - The checker outputs "C" if the output is correct, or "I " if the output is incorrect. The optional data can include any information useful for understanding why the output is wrong and will be shown when errors are displayed 32 | -t, --timeout 33 | The number of seconds after which a test or generation (or checker if you're using the --checker flag) times out if the program does not return. WARNING: if you're using the sio2jail flag, this timeout will still work based on time measured directly by toster, not time measured by sio2jail [default: 5] 34 | --compile-timeout 35 | The number of seconds after which compilation times out if it doesn't finish [default: 10] 36 | --compile-command 37 | The command used to compile the file. gets replaced with the path to the source code file, is the executable output location [default: "g++ -std=c++20 -O3 -static -o "] 38 | -s, --sio2jail 39 | Makes toster use sio2jail for measuring program runtime and memory use more accurately. By default limits memory use to 1 GiB. WARNING: enabling this flag can significantly slow down testing 40 | -m, --memory-limit 41 | Sets a memory limit (in KiB) for the executed program and enables the sio2jail flag. WARNING: enabling this flag can significantly slow down testing 42 | -g, --generate 43 | Makes toster generate output files in the output directory instead of comparing the program's output with the files in the output directory 44 | -h, --help 45 | Print help 46 | -V, --version 47 | Print version 48 | ``` 49 | 50 | # Compiler 51 | If you're using the sio2jail feature and want to make sure that your toster measurements are exactly identical to those of sio2 on a contest, you need to make sure that you're using the same compiler version as the one used in sio. The compiler used in the [Polish Olympiad in Informatics](https://www.oi.edu.pl/) as of XXXI OI is G++ 12.2 (as detailed [here](https://www.oi.edu.pl/l/31oi_ustalenia_techniczne/)). If you want to install G++ 12.2, you can do so by building it from scratch (for example using [this](https://github.com/darrenjs/howto/blob/master/build_scripts/build_gcc_10.sh) script, only changing the version). You can also download prebuilt G++ versions made by me from here: 52 | - [G++ 10.2](https://mikolek.com/gcc-10.2) 53 | - [G++ 12.2](https://mikolek.com/gcc-12.2) 54 | 55 | # License 56 | Toster is licensed under the [MIT Licence](https://github.com/MikolajKolek/toster/blob/master/LICENSE) 57 | 58 | # Dependencies 59 | Toster uses [sio2jail](https://github.com/sio2project/sio2jail), a project available under the MIT licence 60 | -------------------------------------------------------------------------------- /src/compiler.rs: -------------------------------------------------------------------------------- 1 | use std::{fs, io}; 2 | use std::io::ErrorKind::NotFound; 3 | use std::io::{read_to_string, Seek}; 4 | use std::path::{Path, PathBuf}; 5 | use std::process::Command; 6 | use std::time::{Duration, Instant}; 7 | use colored::Colorize; 8 | use is_executable::is_executable; 9 | use tempfile::TempDir; 10 | use wait_timeout::ChildExt; 11 | use crate::compiler::CompilerError::{CompilationError, InvalidExecutable}; 12 | use crate::formatted_error::FormattedError; 13 | use crate::temp_files::{create_temp_file, make_cloned_stdio}; 14 | 15 | pub(crate) enum CompilerError { 16 | InvalidExecutable(io::Error), 17 | CompilationError(String), 18 | } 19 | 20 | impl CompilerError { 21 | pub fn to_formatted(&self, is_checker: bool) -> FormattedError { 22 | FormattedError::preformatted(match self { 23 | InvalidExecutable(error) => { 24 | format!( 25 | "{}\n{}", 26 | format!( 27 | "The provided {} can't be executed", 28 | if is_checker { "checker" } else { "program" } 29 | ).red(), 30 | error 31 | ) 32 | }, 33 | CompilationError(error) => { 34 | format!( 35 | "{}\n{}", 36 | format!( 37 | "{} compilation failed with the following errors:", 38 | if is_checker { "Checker" } else { "Program" } 39 | ).red(), 40 | error 41 | ) 42 | } 43 | }) 44 | } 45 | } 46 | 47 | pub(crate) struct Compiler<'a> { 48 | pub(crate) tempdir: &'a TempDir, 49 | pub(crate) compile_timeout: Duration, 50 | pub(crate) compile_command: &'a str, 51 | } 52 | 53 | impl<'a> Compiler<'a> { 54 | fn is_source_file(path: &Path) -> bool { 55 | if let Some(extension) = path.extension().and_then(|extension| extension.to_str()) { 56 | return matches!(extension, "cpp" | "cc" | "cxx" | "c"); 57 | } 58 | !is_executable(path) 59 | } 60 | 61 | fn compile_cpp(&self, source_path: &Path, executable_path: &Path) -> Result { 62 | let cmd = self.compile_command 63 | .replace("", source_path.to_str().expect("The provided filename is invalid")) 64 | .replace("", executable_path.to_str().expect("The provided filename is invalid")); 65 | let mut split_cmd = cmd.split(' '); 66 | 67 | let mut stderr = create_temp_file().expect("Failed to create memfile"); 68 | let time_before_compilation = Instant::now(); 69 | let child = Command::new(split_cmd.next().expect("The compile command is invalid")) 70 | .args(split_cmd) 71 | .stderr(make_cloned_stdio(&stderr)) 72 | .spawn(); 73 | 74 | let mut child = match child { 75 | Ok(child) => child, 76 | Err(error) if error.kind() == NotFound => { return Err("The compiler was not found".to_string()) } 77 | Err(error) => { return Err(error.to_string()) } 78 | }; 79 | let result = child.wait_timeout(self.compile_timeout).unwrap(); 80 | 81 | stderr.rewind().unwrap(); 82 | 83 | match result { 84 | Some(status) => { 85 | if status.code().expect("The compiler returned an invalid status code") != 0 { 86 | let compilation_result = read_to_string(stderr).expect("Failed to read compiler output"); 87 | return Err(compilation_result); 88 | } 89 | } 90 | None => { 91 | child.kill().unwrap(); 92 | return Err("Compilation timed out".to_string()); 93 | } 94 | } 95 | Ok(time_before_compilation.elapsed()) 96 | } 97 | 98 | fn try_spawning_executable(executable_path: &PathBuf) -> io::Result<()> { 99 | Command::new(executable_path) 100 | .spawn() 101 | .map(|mut child| { 102 | child.kill().expect("Failed to kill executable"); 103 | }) 104 | } 105 | 106 | pub(crate) fn prepare_executable( 107 | &self, 108 | source_path: &Path, 109 | name: &'static str, 110 | ) -> Result<(PathBuf, Option), CompilerError> { 111 | debug_assert!(PathBuf::from(name).extension().is_none()); 112 | let output_path = self.tempdir.path().join(format!("{}.o", name)); 113 | 114 | if !Self::is_source_file(source_path) { 115 | fs::copy(source_path, &output_path).expect("The provided filename is invalid"); 116 | if let Err(error) = Self::try_spawning_executable(&output_path) { 117 | return Err(InvalidExecutable(error)); 118 | } 119 | return Ok((output_path, None)); 120 | } 121 | 122 | match self.compile_cpp(source_path, &output_path) { 123 | Ok(compilation_time) => Ok((output_path, Some(compilation_time))), 124 | Err(error) => Err(CompilationError(error)), 125 | } 126 | } 127 | } -------------------------------------------------------------------------------- /src/test_summary.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Ordering; 2 | use std::time::{Duration, Instant}; 3 | use colored::Color::{Blue, Green, Red, Yellow}; 4 | use colored::{Color, Colorize}; 5 | use crate::test_errors::{ExecutionError, ExecutionMetrics, TestError}; 6 | use crate::test_errors::TestError::*; 7 | 8 | pub(crate) struct TestSummary { 9 | pub(crate) generate_mode: bool, 10 | pub(crate) start_time: Instant, 11 | 12 | pub(crate) total: usize, 13 | pub(crate) processed: usize, 14 | pub(crate) success: usize, 15 | pub(crate) incorrect: usize, 16 | pub(crate) timed_out: usize, 17 | pub(crate) invalid_output: usize, 18 | pub(crate) memory_limit_exceeded: usize, 19 | pub(crate) runtime_error: usize, 20 | pub(crate) sio2jail_error: usize, 21 | pub(crate) checker_error: usize, 22 | pub(crate) no_output_file: usize, 23 | 24 | test_errors: Vec<(String, TestError)>, 25 | 26 | pub(crate) slowest_test: Option<(Duration, String)>, 27 | pub(crate) most_memory_used: Option<(u64, String)>, 28 | } 29 | 30 | struct CountPart<'a> { 31 | display_empty: bool, 32 | count: usize, 33 | singular: &'a str, 34 | plural: &'a str, 35 | color: Color, 36 | } 37 | 38 | impl<'a> CountPart<'a> { 39 | fn new(count: usize, text: &'a str) -> Self { 40 | CountPart { 41 | display_empty: false, 42 | count, 43 | singular: text, 44 | plural: text, 45 | color: Red 46 | } 47 | } 48 | 49 | fn with_plural(mut self, text: &'a str) -> Self { 50 | self.plural = text; 51 | self 52 | } 53 | 54 | fn display_empty(mut self) -> Self { 55 | self.display_empty = true; 56 | self 57 | } 58 | 59 | fn with_color(mut self, color: Color) -> Self { 60 | self.color = color; 61 | self 62 | } 63 | 64 | fn get_text(&self) -> &str { 65 | if self.count == 1 { self.singular } 66 | else { self.plural } 67 | } 68 | } 69 | 70 | impl TestSummary { 71 | pub(crate) fn new(generate_mode: bool, total_count: usize) -> Self { 72 | TestSummary { 73 | generate_mode, 74 | start_time: Instant::now(), 75 | 76 | total: total_count, 77 | processed: 0, 78 | incorrect: 0, 79 | timed_out: 0, 80 | invalid_output: 0, 81 | memory_limit_exceeded: 0, 82 | runtime_error: 0, 83 | sio2jail_error: 0, 84 | checker_error: 0, 85 | no_output_file: 0, 86 | success: 0, 87 | 88 | test_errors: vec![], 89 | 90 | slowest_test: None, 91 | most_memory_used: None, 92 | } 93 | } 94 | 95 | pub(crate) fn add_success(&mut self, metrics: &ExecutionMetrics, test_name: &str) { 96 | self.processed += 1; 97 | self.success += 1; 98 | self.add_metrics(metrics, test_name); 99 | } 100 | 101 | pub(crate) fn add_test_error(&mut self, error: TestError, test_name: String) { 102 | match &error { 103 | Incorrect { .. } => { self.incorrect += 1 } 104 | ProgramError { error: ExecutionError::TimedOut, .. } => { self.timed_out += 1 } 105 | ProgramError { error: ExecutionError::MemoryLimitExceeded, .. } => { self.memory_limit_exceeded += 1 } 106 | ProgramError { error: ExecutionError::RuntimeError(_), .. } => { self.runtime_error += 1 } 107 | ProgramError { error: ExecutionError::Sio2jailError(_), .. } => { self.sio2jail_error += 1 } 108 | ProgramError { error: ExecutionError::IncorrectCheckerFormat(_), .. } => { self.checker_error += 1 } 109 | ProgramError { error: ExecutionError::PipeError } => { self.invalid_output += 1 } 110 | ProgramError { error: ExecutionError::OutputNotUtf8 } => { self.invalid_output += 1 } 111 | CheckerError { .. } => { self.checker_error += 1 } 112 | NoOutputFile { .. } => { self.no_output_file += 1 } 113 | Cancelled => return, 114 | } 115 | self.processed += 1; 116 | self.test_errors.push((test_name, error)); 117 | } 118 | 119 | fn add_metrics(&mut self, metrics: &ExecutionMetrics, test_name: &str) { 120 | if let Some(new_time) = &metrics.time { 121 | if self.slowest_test.as_ref().is_none_or(|(time, _)| new_time > time) { 122 | self.slowest_test = Some((*new_time, test_name.to_string())); 123 | } 124 | } 125 | 126 | if let Some(new_memory) = &metrics.memory_kibibytes { 127 | if self.most_memory_used.as_ref().is_none_or(|(memory, _)| new_memory > memory) { 128 | self.most_memory_used = Some((*new_memory, test_name.to_string())); 129 | } 130 | } 131 | } 132 | 133 | pub(crate) fn format_counts(&self, show_not_finished: bool) -> String { 134 | [ 135 | CountPart::new(self.success, if self.generate_mode { "successful" } else { "correct" }).display_empty().with_color(Green), 136 | CountPart::new(self.incorrect, "wrong answer").with_plural("wrong answers"), 137 | CountPart::new(self.timed_out, "timed out"), 138 | CountPart::new(self.invalid_output, "invalid output").with_plural("invalid outputs"), 139 | CountPart::new(self.memory_limit_exceeded, "out of memory"), 140 | CountPart::new(self.runtime_error, "runtime error").with_plural("runtime errors"), 141 | CountPart::new(self.no_output_file, "without output file"), 142 | CountPart::new(self.sio2jail_error, "sio2jail error").with_plural("sio2jail errors"), 143 | CountPart::new(self.checker_error, "checker error").with_plural("checker errors").with_color(Blue), 144 | CountPart::new(if show_not_finished { self.total - self.processed } else { 0 }, "not finished").with_color(Yellow), 145 | ] 146 | .into_iter() 147 | .filter(|part| part.display_empty || part.count > 0) 148 | .map(|part| { 149 | format!("{} {}", part.count, part.get_text()).color(part.color).to_string() 150 | }) 151 | .collect::>() 152 | .join(", ") 153 | } 154 | 155 | pub(crate) fn get_errors(&mut self) -> &Vec<(String, TestError)> { 156 | self.test_errors.sort_by(|a, b| -> Ordering { 157 | human_sort::compare(&a.0, &b.0) 158 | }); 159 | &self.test_errors 160 | } 161 | } -------------------------------------------------------------------------------- /src/args.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use std::time::Duration; 3 | use clap::Parser; 4 | use crate::args::ExecuteMode::{Simple}; 5 | 6 | #[derive(Parser, Debug)] 7 | #[command(name = "Toster", version, about = "A simple-as-toast tester for C++ solutions to competitive programming exercises\nReport issues on the bugtracker at https://github.com/MikolajKolek/toster/issues", long_about = None)] 8 | pub struct Args { 9 | /// Input directory 10 | #[clap(short, long, value_parser, default_value = "in")] 11 | pub r#in: PathBuf, 12 | 13 | /// Input file extension 14 | #[clap(long, value_parser, default_value = ".in")] 15 | pub in_ext: String, 16 | 17 | /// Output directory 18 | #[clap(short, long, value_parser, default_value = "out")] 19 | pub out: PathBuf, 20 | 21 | /// Output file extension 22 | #[clap(long, value_parser, default_value = ".out")] 23 | pub out_ext: String, 24 | 25 | /// The input and output directory (sets both -i and -o at once) 26 | #[clap(long, value_parser)] 27 | pub io: Option, 28 | 29 | /// The C++ source code or executable of a checker program that verifies if the tested program's output is correct instead of comparing it with given output files 30 | /// The checker must use the following protocol: 31 | /// - The checker receives the contents of the input file and the output of the tested program on stdin, separated by a single "\n" character 32 | /// - The checker outputs "C" if the output is correct, or "I " if the output is incorrect. The optional data can include any information useful for understanding why the output is wrong and will be shown when errors are displayed 33 | #[clap(short, long, value_parser, verbatim_doc_comment)] 34 | pub checker: Option, 35 | 36 | /// The number of seconds after which a test or generation times out if the program does not return 37 | #[cfg(not(all(target_os = "linux", target_arch = "x86_64")))] 38 | #[clap(short, long, value_parser, default_value = "5")] 39 | pub timeout: u64, 40 | 41 | /// The number of seconds after which a test or generation (or checker if you're using the --checker flag) times out if the program does not return. WARNING: if you're using the sio2jail flag, this timeout will still work based on time measured directly by toster, not time measured by sio2jail 42 | #[cfg(all(target_os = "linux", target_arch = "x86_64"))] 43 | #[clap(short, long, value_parser, default_value = "5")] 44 | pub timeout: u64, 45 | 46 | /// The number of seconds after which compilation times out if it doesn't finish 47 | #[clap(long, value_parser, default_value = "10")] 48 | pub compile_timeout: u64, 49 | 50 | /// The command used to compile the file. gets replaced with the path to the source code file, is the executable output location. 51 | #[clap(long, value_parser, default_value = "g++ -std=c++20 -O3 -static -o ")] 52 | pub compile_command: String, 53 | 54 | /// Makes toster use sio2jail for measuring program runtime and memory use more accurately. By default limits memory use to 1 GiB. WARNING: enabling this flag can significantly slow down testing 55 | #[cfg(all(target_os = "linux", target_arch = "x86_64"))] 56 | #[clap(short, long, action)] 57 | pub sio2jail: bool, 58 | 59 | /// Sets a memory limit (in KiB) for the executed program and enables the sio2jail flag. WARNING: enabling this flag can significantly slow down testing 60 | #[cfg(all(target_os = "linux", target_arch = "x86_64"))] 61 | #[clap(short, long, value_parser)] 62 | pub memory_limit: Option, 63 | 64 | /// Makes toster generate output files in the output directory instead of comparing the program's output with the files in the output directory 65 | #[clap(short, long, action)] 66 | pub generate: bool, 67 | 68 | /// The name of the file containing the source code or the executable you want to test 69 | #[clap(value_parser)] 70 | pub filename: PathBuf 71 | } 72 | 73 | pub(crate) enum InputConfig { 74 | Directory { 75 | directory: PathBuf, 76 | ext: String, 77 | } 78 | } 79 | 80 | pub(crate) enum ExecuteMode { 81 | Simple, 82 | #[cfg(all(target_os = "linux", target_arch = "x86_64"))] 83 | Sio2jail { 84 | memory_limit: u64, 85 | } 86 | } 87 | 88 | pub(crate) enum ActionType { 89 | Generate { 90 | output_directory: PathBuf, 91 | output_ext: String, 92 | }, 93 | SimpleCompare { 94 | output_directory: PathBuf, 95 | output_ext: String, 96 | }, 97 | Checker { 98 | path: PathBuf, 99 | } 100 | } 101 | 102 | pub(crate) struct ParsedConfig { 103 | pub(crate) source_path: PathBuf, 104 | pub(crate) compile_command: String, 105 | pub(crate) compile_timeout: Duration, 106 | pub(crate) execute_timeout: Duration, 107 | pub(crate) input: InputConfig, 108 | pub(crate) execute_mode: ExecuteMode, 109 | pub(crate) action_type: ActionType, 110 | } 111 | 112 | fn verify_compile_command(command: &str) -> Result<(), String> { 113 | let message = format!( 114 | "The compile command is invalid:\n{}\nRead \"toster -h\" for more info", 115 | match (command.contains(""), command.contains("")) { 116 | (true, true) => return Ok(()), 117 | (false, true) => "The argument is missing\n", 118 | (true, false) => "The argument is missing\n", 119 | (false, false) => "The and arguments are missing\n", 120 | } 121 | ); 122 | Err(message) 123 | } 124 | 125 | impl TryFrom for ParsedConfig { 126 | type Error = String; 127 | 128 | fn try_from(args: Args) -> Result { 129 | if !args.filename.is_file() { 130 | return Err("The provided file does not exist".to_string()); 131 | } 132 | 133 | let (input_directory, output_directory) = match args.io { 134 | Some(io) => { 135 | if !io.is_dir() { 136 | return Err("The input/output directory does not exist".to_string()); 137 | } 138 | (io.clone(), io) 139 | }, 140 | None => { 141 | if !args.r#in.is_dir() { 142 | return Err("The input directory does not exist".to_string()); 143 | } 144 | (args.r#in, args.out) 145 | } 146 | }; 147 | 148 | verify_compile_command(&args.compile_command)?; 149 | 150 | Ok(ParsedConfig { 151 | source_path: args.filename, 152 | compile_timeout: Duration::from_secs(args.compile_timeout), 153 | execute_timeout: Duration::from_secs(args.timeout), 154 | compile_command: args.compile_command, 155 | input: InputConfig::Directory { 156 | directory: input_directory, 157 | ext: args.in_ext, 158 | }, 159 | 160 | action_type: match (args.generate, args.checker) { 161 | (true, Some(_)) => { 162 | return Err("You can't have the --generate and --checker flags on at the same time".to_string()) 163 | }, 164 | (true, None) => { 165 | if output_directory.exists() && !output_directory.is_dir() { 166 | return Err("The output path is not a directory".to_string()) 167 | } 168 | ActionType::Generate { 169 | output_directory, 170 | output_ext: args.out_ext, 171 | } 172 | }, 173 | (false, None) => { 174 | if !output_directory.is_dir() { 175 | return Err("The output directory does not exist".to_string()) 176 | } 177 | ActionType::SimpleCompare { 178 | output_directory, 179 | output_ext: args.out_ext, 180 | } 181 | }, 182 | (false, Some(checker_path)) => { 183 | if !checker_path.is_file() { 184 | return Err("The provided checker file does not exist".to_string()); 185 | } 186 | ActionType::Checker { 187 | path: checker_path, 188 | } 189 | } 190 | }, 191 | 192 | execute_mode: { 193 | #[cfg(all(target_os = "linux", target_arch = "x86_64"))] { 194 | if let Some(memory_limit) = args.memory_limit { 195 | ExecuteMode::Sio2jail { memory_limit } 196 | } else if args.sio2jail { 197 | ExecuteMode::Sio2jail { memory_limit: 1024 * 1204 } 198 | } else { 199 | Simple 200 | } 201 | } 202 | #[cfg(not(all(target_os = "linux", target_arch = "x86_64")))] 203 | Simple 204 | } 205 | }) 206 | } 207 | } 208 | 209 | impl ParsedConfig { 210 | pub(crate) fn generate_mode(&self) -> bool { 211 | matches!(self.action_type, ActionType::Generate { .. }) 212 | } 213 | } -------------------------------------------------------------------------------- /src/executor/sio2jail.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::{read_to_string, Seek}; 3 | use std::os::unix::process::ExitStatusExt; 4 | use std::path::{Path, PathBuf}; 5 | use std::process::{Command, ExitStatus}; 6 | use std::time::Duration; 7 | use colored::Colorize; 8 | use command_fds::{CommandFdExt, FdMapping}; 9 | use directories::BaseDirs; 10 | use wait_timeout::ChildExt; 11 | use which::which; 12 | use crate::temp_files::{create_temp_file, make_cloned_stdio}; 13 | use crate::executor::TestExecutor; 14 | use crate::formatted_error::FormattedError; 15 | use crate::generic_utils::halt; 16 | use crate::test_errors::{ExecutionError, ExecutionMetrics}; 17 | use crate::test_errors::ExecutionError::{MemoryLimitExceeded, RuntimeError, Sio2jailError, TimedOut}; 18 | 19 | pub(crate) struct Sio2jailExecutor { 20 | timeout: Duration, 21 | executable_path: PathBuf, 22 | sio2jail_path: PathBuf, 23 | memory_limit: u64, 24 | } 25 | 26 | struct Sio2jailOutput { 27 | status: ExitStatus, 28 | stderr: String, 29 | sio2jail_output: String, 30 | } 31 | 32 | impl Sio2jailExecutor { 33 | fn get_sio2jail_path() -> Result { 34 | let Some(binding) = BaseDirs::new() else { 35 | return Err(FormattedError::from_str( 36 | "No valid home directory path could be retrieved from the operating system. Sio2jail could not be found" 37 | )); 38 | }; 39 | let Some(executable_dir) = binding.executable_dir() else { 40 | return Err(FormattedError::from_str( 41 | "Couldn't locate the user's executable directory. Sio2jail could not be found" 42 | )); 43 | }; 44 | 45 | let result = executable_dir.join("sio2jail"); 46 | if !result.exists() { 47 | return Err(FormattedError::from_str( 48 | &format!("Sio2jail could not be found at {}", result.display()) 49 | )); 50 | } 51 | Ok(result) 52 | } 53 | 54 | fn run_sio2jail(&self, input_file: &File, output_file: &File, executable_path: &Path) -> Result { 55 | let mut sio2jail_output = create_temp_file().unwrap(); 56 | let mut stderr = create_temp_file().unwrap(); 57 | 58 | let mut child = Command::new(&self.sio2jail_path) 59 | .args(["-f", "3", "-o", "oiaug", "--mount-namespace", "off", "--pid-namespace", "off", "--uts-namespace", "off", "--ipc-namespace", "off", "--net-namespace", "off", "--capability-drop", "off", "--user-namespace", "off", "-m", &self.memory_limit.to_string(), "--", executable_path.to_str().unwrap() ]) 60 | .fd_mappings(vec![FdMapping { 61 | parent_fd: sio2jail_output.try_clone().unwrap().into(), 62 | child_fd: 3 63 | }]).expect("Failed to redirect file descriptor 3") 64 | .stdout(make_cloned_stdio(output_file)) 65 | .stderr(make_cloned_stdio(&stderr)) 66 | .stdin(make_cloned_stdio(input_file)) 67 | .spawn().expect("Failed to spawn sio2jail"); 68 | 69 | let status = child.wait_timeout(self.timeout).unwrap(); 70 | let Some(status) = status else { 71 | child.kill().unwrap(); 72 | return Err(TimedOut); 73 | }; 74 | 75 | sio2jail_output.rewind().unwrap(); 76 | stderr.rewind().unwrap(); 77 | 78 | Ok(Sio2jailOutput { 79 | status, 80 | stderr: read_to_string(stderr).unwrap(), 81 | sio2jail_output: read_to_string(sio2jail_output).unwrap(), 82 | }) 83 | } 84 | 85 | fn test(&self) -> Result<(), FormattedError> { 86 | let Ok(true_command_location) = which("true") else { 87 | return Err(FormattedError::from_str("The executable for the \"true\" command could not be found")); 88 | }; 89 | 90 | let null_file = File::open("/dev/null").expect("Opening /dev/null should not fail"); 91 | let output = self.run_sio2jail(&null_file, &null_file, &true_command_location); 92 | let output = match output { 93 | Ok(output) => output, 94 | Err(error) => { 95 | return Err(FormattedError::from_str(&format!("Sio2jail error: {}", error.to_string()))); 96 | } 97 | }; 98 | if output.stderr == "Exception occurred: System error occured: perf event open failed: Permission denied: error 13: Permission denied\n" { 99 | return Err(FormattedError::preformatted(format!( 100 | "{}\n{}", 101 | "You need to run the following command to use toster with sio2jail.\n\ 102 | You may also put this option in your /etc/sysctl.conf.\n\ 103 | This will make the setting persist across reboots.".red(), 104 | "sudo sysctl -w kernel.perf_event_paranoid=-1".white() 105 | ))); 106 | } 107 | if !output.stderr.is_empty() { 108 | return Err(FormattedError::from_str(&format!("Sio2jail error: {}", output.stderr))); 109 | } 110 | Ok(()) 111 | } 112 | 113 | pub(crate) fn init_and_test(timeout: Duration, executable_path: PathBuf, memory_limit: u64) -> Result { 114 | let executor = Sio2jailExecutor { 115 | timeout, 116 | memory_limit, 117 | executable_path, 118 | sio2jail_path: Self::get_sio2jail_path()?, 119 | }; 120 | executor.test()?; 121 | Ok(executor) 122 | } 123 | } 124 | 125 | impl TestExecutor for Sio2jailExecutor { 126 | fn test_to_file(&self, input_file: &File, output_file: &File) -> (ExecutionMetrics, Result<(), ExecutionError>) { 127 | let output = match self.run_sio2jail(input_file, output_file, &self.executable_path) { 128 | Err(TimedOut) => { 129 | return (ExecutionMetrics { time: Some(self.timeout), memory_kibibytes: None }, Err(TimedOut)); 130 | } 131 | Err(error) => { 132 | return (ExecutionMetrics::NONE, Err(error)); 133 | } 134 | Ok(output) => output 135 | }; 136 | 137 | if !output.stderr.is_empty() { 138 | return if output.stderr == "terminate called after throwing an instance of 'std::bad_alloc'\n what(): std::bad_alloc\n" { 139 | (ExecutionMetrics { time: None, memory_kibibytes: Some(self.memory_limit) }, Err(MemoryLimitExceeded)) 140 | } else { 141 | (ExecutionMetrics::NONE, Err(Sio2jailError(output.stderr))) 142 | } 143 | } 144 | 145 | let split: Vec<&str> = output.sio2jail_output.split_whitespace().collect(); 146 | if split.len() < 6 { 147 | return (ExecutionMetrics::NONE, Err(Sio2jailError(format!("The sio2jail output is too short: {}", output.sio2jail_output)))); 148 | } 149 | let sio2jail_status = split[0]; 150 | let time = Duration::from_secs_f64(split[2].parse::().expect("Sio2jail returned an invalid runtime in the output") / 1000.0); 151 | let memory_kibibytes = split[4].parse::().expect("Sio2jail returned invalid memory usage in the output"); 152 | let error_message = output.sio2jail_output.lines().nth(1); 153 | 154 | let metrics = ExecutionMetrics { 155 | time: Some(time), 156 | memory_kibibytes: Some(memory_kibibytes) 157 | }; 158 | 159 | match output.status.code() { 160 | None => { 161 | #[cfg(unix)] 162 | if cfg!(unix) && output.status.signal().expect("Sio2jail returned an invalid status code") == 2 { 163 | halt(); 164 | } 165 | 166 | return (metrics, Err(RuntimeError(format!("- the process was terminated with the following error:\n{}", output.status)))) 167 | } 168 | Some(0) => {} 169 | Some(exit_code) => { 170 | return (metrics, Err(Sio2jailError(format!("Sio2jail returned an invalid status code: {}", exit_code))) ); 171 | } 172 | } 173 | 174 | (ExecutionMetrics { time: Some(time), memory_kibibytes: Some(memory_kibibytes) }, match sio2jail_status { 175 | "OK" => Ok(()), 176 | "RE" | "RV" => Err(RuntimeError(error_message.map(|message| format!("- {}", message)).unwrap_or(String::new()))), 177 | "TLE" => Err(TimedOut), 178 | "MLE" => Err(MemoryLimitExceeded), 179 | "OLE" => Err(RuntimeError("- output limit exceeded".to_string())), 180 | _ => Err(Sio2jailError(format!("Sio2jail returned an invalid status in the output: {}", sio2jail_status))) 181 | }) 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod args; 2 | mod test_errors; 3 | mod testing_utils; 4 | mod prepare_input; 5 | mod executor; 6 | mod generic_utils; 7 | mod test_summary; 8 | mod temp_files; 9 | mod checker; 10 | mod compiler; 11 | mod formatted_error; 12 | 13 | use std::{fs, panic}; 14 | use std::fmt::Write as FmtWrite; 15 | use std::fs::File; 16 | use std::panic::PanicHookInfo; 17 | use std::path::PathBuf; 18 | use std::process::{exit, ExitCode}; 19 | use std::sync::{Arc, Mutex}; 20 | use std::sync::atomic::AtomicBool; 21 | use std::sync::atomic::Ordering::{Acquire, Release}; 22 | use clap::Parser; 23 | use colored::Colorize; 24 | use human_panic::{handle_dump, print_msg}; 25 | use indicatif::{ParallelProgressIterator, ProgressBar, ProgressState, ProgressStyle}; 26 | use rayon::prelude::*; 27 | use tempfile::tempdir; 28 | use args::Args; 29 | use crate::args::{ActionType, InputConfig, ParsedConfig}; 30 | use crate::args::ExecuteMode::*; 31 | use crate::checker::Checker; 32 | use crate::compiler::Compiler; 33 | use crate::executor::simple::SimpleExecutor; 34 | use crate::prepare_input::{prepare_file_inputs, Test, TestingInputs}; 35 | use crate::executor::{AnyTestExecutor, test_to_temp, TestExecutor}; 36 | use crate::test_errors::{ExecutionMetrics, TestError}; 37 | use crate::test_errors::TestError::{Cancelled, ProgramError}; 38 | use crate::test_summary::TestSummary; 39 | use crate::testing_utils::compare_output; 40 | #[cfg(all(target_os = "linux", target_arch = "x86_64"))] 41 | use crate::executor::sio2jail::Sio2jailExecutor; 42 | use crate::formatted_error::FormattedError; 43 | use crate::generic_utils::halt; 44 | 45 | static RECEIVED_CTRL_C: AtomicBool = AtomicBool::new(false); 46 | 47 | fn print_output(stopped_early: bool, test_summary: &mut Option) { 48 | let Some(test_summary) = test_summary else { 49 | println!("{}", "Toster was stopped before testing could start".red()); 50 | exit(0); 51 | }; 52 | 53 | if stopped_early { 54 | println!(); 55 | } 56 | 57 | let additional_info = match (&test_summary.slowest_test, &test_summary.most_memory_used) { 58 | (None, None) => "".to_string(), 59 | (Some((duration, slowest_test_name)), None) => format!( 60 | " (Slowest test: {} at {:.3}s)", 61 | slowest_test_name, duration.as_secs_f32(), 62 | ), 63 | (None, Some((memory, most_memory_test_name))) => format!( 64 | " (Most memory used: {} at {:.3}KiB)", 65 | most_memory_test_name, memory, 66 | ), 67 | (Some((duration, slowest_test_name)), Some((memory, most_memory_test_name))) => format!( 68 | " (Slowest test: {} at {:.3}s, most memory used: {} at {}KiB)", 69 | slowest_test_name, duration.as_secs_f32(), 70 | most_memory_test_name, memory, 71 | ), 72 | }; 73 | 74 | println!( 75 | "{} {} {:.2}s{}\nResults: {}", 76 | if test_summary.generate_mode { "Generating" } else { "Testing" }, 77 | if stopped_early {"stopped after"} else {"finished in"}, 78 | test_summary.start_time.elapsed().as_secs_f64(), 79 | additional_info, 80 | test_summary.format_counts(true), 81 | ); 82 | 83 | let incorrect_results = test_summary.get_errors(); 84 | if !incorrect_results.is_empty() { 85 | println!("Errors were found in the following tests:"); 86 | 87 | for (test_name, error) in incorrect_results.iter() { 88 | println!("{}", error.to_string(test_name)); 89 | } 90 | } 91 | 92 | exit(0); 93 | } 94 | 95 | fn setup_panic() { 96 | let is_panicking = AtomicBool::new(false); 97 | match human_panic::PanicStyle::default() { 98 | human_panic::PanicStyle::Debug => {} 99 | human_panic::PanicStyle::Human => { 100 | let meta = human_panic::metadata!(); 101 | 102 | panic::set_hook(Box::new(move |info: &PanicHookInfo| { 103 | if is_panicking.load(Acquire) { 104 | halt(); 105 | } 106 | is_panicking.store(true, Release); 107 | 108 | let file_path = handle_dump(&meta, info); 109 | print_msg(file_path, &meta).expect("human-panic: printing error message to console failed"); 110 | exit(0); 111 | })); 112 | }, 113 | _ => {} 114 | } 115 | } 116 | 117 | fn check_ctrlc() -> Result<(), TestError> { 118 | if RECEIVED_CTRL_C.load(Acquire) { Err(Cancelled) } 119 | else { Ok(()) } 120 | } 121 | 122 | fn init_runner(executable: PathBuf, config: &ParsedConfig) -> Result { 123 | Ok(match config.execute_mode { 124 | Simple => AnyTestExecutor::Simple(SimpleExecutor { 125 | executable_path: executable, 126 | timeout: config.execute_timeout, 127 | }), 128 | #[cfg(all(target_os = "linux", target_arch = "x86_64"))] 129 | Sio2jail { memory_limit } => AnyTestExecutor::Sio2Jail(Sio2jailExecutor::init_and_test( 130 | config.execute_timeout, 131 | executable, 132 | memory_limit, 133 | )?), 134 | }) 135 | } 136 | 137 | fn map_tests( 138 | inputs: TestingInputs, 139 | progress_bar: ProgressBar, 140 | test_summary: &Arc>>, 141 | callback: impl Fn(Test) -> Result + Sync 142 | ) where T: IndexedParallelIterator { 143 | inputs.iterator.progress_with(progress_bar).try_for_each(|input| { 144 | let test_name = input.test_name.clone(); 145 | 146 | let result = callback(input); 147 | 148 | let mut test_summary = test_summary.lock().expect("Failed to lock test summary mutex"); 149 | let test_summary = test_summary.as_mut().unwrap(); 150 | match result { 151 | Ok(metrics) => test_summary.add_success(&metrics, &test_name), 152 | Err(Cancelled) => return None, 153 | Err(error) => test_summary.add_test_error(error, test_name), 154 | }; 155 | Some(()) 156 | }); 157 | } 158 | 159 | fn main() -> ExitCode { 160 | setup_panic(); 161 | 162 | if let Err(error) = try_main() { 163 | println!("{}", error); 164 | return ExitCode::FAILURE; 165 | } 166 | ExitCode::SUCCESS 167 | } 168 | 169 | fn try_main() -> Result<(), FormattedError> { 170 | let config = ParsedConfig::try_from(Args::parse()) 171 | .map_err(|error| FormattedError::from_str(&error))?; 172 | let test_summary: Arc>> = Arc::new(Mutex::new(None)); 173 | { 174 | let test_summary = test_summary.clone(); 175 | ctrlc::set_handler(move || { 176 | RECEIVED_CTRL_C.store(true, Release); 177 | print_output(true, &mut test_summary.lock().expect("Failed to lock test summary mutex")); 178 | }).expect("Error setting Ctrl-C handler"); 179 | } 180 | 181 | let tempdir = tempdir().expect("Failed to create temporary directory"); 182 | 183 | if let ActionType::Generate { output_directory, .. } = &config.action_type { 184 | if !output_directory.is_dir() { 185 | fs::create_dir_all(output_directory).expect("Failed to create output directory"); 186 | } 187 | } 188 | 189 | let compiler = Compiler { 190 | tempdir: &tempdir, 191 | compile_timeout: config.compile_timeout, 192 | compile_command: &config.compile_command, 193 | }; 194 | 195 | let executable = { 196 | let (executable, compilation_time) = compiler 197 | .prepare_executable(&config.source_path, "program") 198 | .map_err(|error| error.to_formatted(false))?; 199 | if let Some(compilation_time) = compilation_time { 200 | println!("{}", format!("Program compilation completed in {:.2}", compilation_time.as_secs_f32()).green()); 201 | } 202 | executable 203 | }; 204 | 205 | let checker_executable = if let ActionType::Checker { path } = &config.action_type { 206 | let (executable, compilation_time) = compiler 207 | .prepare_executable(path, "checker") 208 | .map_err(|error| error.to_formatted(true))?; 209 | if let Some(compilation_time) = compilation_time { 210 | println!("{}", format!("Checker compilation completed in {:.2}", compilation_time.as_secs_f32()).green()); 211 | } 212 | Some(executable) 213 | } else { None }; 214 | 215 | let runner = init_runner(executable, &config)?; 216 | let checker = checker_executable.map(|checker_executable| { 217 | Checker::new(checker_executable, config.execute_timeout) 218 | }); 219 | 220 | // Progress bar styling 221 | let style: ProgressStyle = { 222 | let test_summary = test_summary.clone(); 223 | ProgressStyle::with_template("[{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len} ({eta})\n{counts} {ctrlc}") 224 | .expect("Progress bar creation failed") 225 | .with_key("eta", |state: &ProgressState, w: &mut dyn FmtWrite| write!(w, "{:.1}s", state.eta().as_secs_f64()).expect("Displaying the progress bar failed")) 226 | .progress_chars("#>-") 227 | .with_key("counts", move |_state: &ProgressState, w: &mut dyn FmtWrite| { 228 | write!(w, "{}", test_summary.lock().expect("Failed to lock test summary mutex").as_ref().unwrap().format_counts(false)).expect("Displaying the progress bar failed") 229 | }) 230 | .with_key("ctrlc", |_state: &ProgressState, w: &mut dyn FmtWrite| 231 | write!(w, "{}", "(Press Ctrl+C to stop testing and print current results)".bright_black()).expect("Displaying the progress bar Ctrl+C message failed") 232 | ) 233 | }; 234 | 235 | let inputs = match &config.input { 236 | InputConfig::Directory { directory, ext } => { 237 | prepare_file_inputs(directory, ext)? 238 | }, 239 | }; 240 | *test_summary.lock().expect("Failed to lock test summary mutex") = Some(TestSummary::new(config.generate_mode(), inputs.test_count)); 241 | 242 | let progress_bar = ProgressBar::new(inputs.test_count as u64).with_style(style); 243 | 244 | match config.action_type { 245 | ActionType::Generate { output_directory, output_ext } => { 246 | map_tests(inputs, progress_bar, &test_summary, |input| { 247 | check_ctrlc()?; 248 | 249 | let output_file_path = output_directory.join(format!("{}{}", input.test_name, &output_ext)); 250 | let file = File::create(output_file_path).expect("Failed to create output file"); 251 | check_ctrlc()?; 252 | 253 | let (metrics, result) = runner.test_to_file(&input.input_source.get_file(), &file); 254 | check_ctrlc()?; 255 | 256 | result.map_err(|error| ProgramError { error })?; 257 | Ok(metrics) 258 | }); 259 | }, 260 | ActionType::SimpleCompare { output_directory, output_ext } => { 261 | map_tests(inputs, progress_bar, &test_summary, |input| { 262 | check_ctrlc()?; 263 | 264 | let (metrics, result) = test_to_temp(&runner, &input.input_source.get_file()); 265 | check_ctrlc()?; 266 | 267 | let result = result.map_err(|error| ProgramError { error })?; 268 | let output_file_path = output_directory.join(format!("{}{}", input.test_name, output_ext)); 269 | compare_output(&output_file_path, result)?; 270 | check_ctrlc()?; 271 | 272 | Ok(metrics) 273 | }); 274 | }, 275 | ActionType::Checker { .. } => { 276 | let checker = checker.expect("Checker should be initialized"); 277 | map_tests(inputs, progress_bar, &test_summary, |input| { 278 | check_ctrlc()?; 279 | 280 | let checker_input = Checker::prepare_checker_input(&input.input_source); 281 | check_ctrlc()?; 282 | 283 | let (metrics, result) = runner.test_to_file( 284 | &input.input_source.get_file(), 285 | &checker_input, 286 | ); 287 | check_ctrlc()?; 288 | 289 | result.map_err(|error| ProgramError { error })?; 290 | checker.check(checker_input)?; 291 | check_ctrlc()?; 292 | 293 | Ok(metrics) 294 | }) 295 | } 296 | } 297 | 298 | print_output(false, &mut test_summary.lock().expect("Failed to lock test summary mutex")); 299 | Ok(()) 300 | } 301 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.21.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "anstream" 22 | version = "0.6.14" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" 25 | dependencies = [ 26 | "anstyle", 27 | "anstyle-parse", 28 | "anstyle-query", 29 | "anstyle-wincon", 30 | "colorchoice", 31 | "is_terminal_polyfill", 32 | "utf8parse", 33 | ] 34 | 35 | [[package]] 36 | name = "anstyle" 37 | version = "1.0.3" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "b84bf0a05bbb2a83e5eb6fa36bb6e87baa08193c35ff52bbf6b38d8af2890e46" 40 | 41 | [[package]] 42 | name = "anstyle-parse" 43 | version = "0.2.1" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" 46 | dependencies = [ 47 | "utf8parse", 48 | ] 49 | 50 | [[package]] 51 | name = "anstyle-query" 52 | version = "1.0.0" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" 55 | dependencies = [ 56 | "windows-sys 0.48.0", 57 | ] 58 | 59 | [[package]] 60 | name = "anstyle-wincon" 61 | version = "3.0.1" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" 64 | dependencies = [ 65 | "anstyle", 66 | "windows-sys 0.48.0", 67 | ] 68 | 69 | [[package]] 70 | name = "autocfg" 71 | version = "1.1.0" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 74 | 75 | [[package]] 76 | name = "backtrace" 77 | version = "0.3.69" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" 80 | dependencies = [ 81 | "addr2line", 82 | "cc", 83 | "cfg-if", 84 | "libc", 85 | "miniz_oxide", 86 | "object", 87 | "rustc-demangle", 88 | ] 89 | 90 | [[package]] 91 | name = "bitflags" 92 | version = "1.3.2" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 95 | 96 | [[package]] 97 | name = "bitflags" 98 | version = "2.4.0" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" 101 | 102 | [[package]] 103 | name = "cc" 104 | version = "1.0.83" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" 107 | dependencies = [ 108 | "libc", 109 | ] 110 | 111 | [[package]] 112 | name = "cfg-if" 113 | version = "1.0.0" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 116 | 117 | [[package]] 118 | name = "cfg_aliases" 119 | version = "0.1.1" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" 122 | 123 | [[package]] 124 | name = "clap" 125 | version = "4.5.4" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" 128 | dependencies = [ 129 | "clap_builder", 130 | "clap_derive", 131 | ] 132 | 133 | [[package]] 134 | name = "clap_builder" 135 | version = "4.5.2" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" 138 | dependencies = [ 139 | "anstream", 140 | "anstyle", 141 | "clap_lex", 142 | "strsim", 143 | ] 144 | 145 | [[package]] 146 | name = "clap_derive" 147 | version = "4.5.4" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" 150 | dependencies = [ 151 | "heck 0.5.0", 152 | "proc-macro2", 153 | "quote", 154 | "syn", 155 | ] 156 | 157 | [[package]] 158 | name = "clap_lex" 159 | version = "0.7.0" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" 162 | 163 | [[package]] 164 | name = "colorchoice" 165 | version = "1.0.0" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 168 | 169 | [[package]] 170 | name = "colored" 171 | version = "2.1.0" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" 174 | dependencies = [ 175 | "lazy_static", 176 | "windows-sys 0.48.0", 177 | ] 178 | 179 | [[package]] 180 | name = "comfy-table" 181 | version = "7.1.1" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "b34115915337defe99b2aff5c2ce6771e5fbc4079f4b506301f5cf394c8452f7" 184 | dependencies = [ 185 | "crossterm", 186 | "strum", 187 | "strum_macros", 188 | "unicode-width", 189 | ] 190 | 191 | [[package]] 192 | name = "command-fds" 193 | version = "0.3.0" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "7bb11bd1378bf3731b182997b40cefe00aba6a6cc74042c8318c1b271d3badf7" 196 | dependencies = [ 197 | "nix 0.27.1", 198 | "thiserror", 199 | ] 200 | 201 | [[package]] 202 | name = "console" 203 | version = "0.15.7" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" 206 | dependencies = [ 207 | "encode_unicode", 208 | "lazy_static", 209 | "libc", 210 | "unicode-width", 211 | "windows-sys 0.45.0", 212 | ] 213 | 214 | [[package]] 215 | name = "crossbeam-deque" 216 | version = "0.8.3" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" 219 | dependencies = [ 220 | "cfg-if", 221 | "crossbeam-epoch", 222 | "crossbeam-utils", 223 | ] 224 | 225 | [[package]] 226 | name = "crossbeam-epoch" 227 | version = "0.9.15" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" 230 | dependencies = [ 231 | "autocfg", 232 | "cfg-if", 233 | "crossbeam-utils", 234 | "memoffset", 235 | "scopeguard", 236 | ] 237 | 238 | [[package]] 239 | name = "crossbeam-utils" 240 | version = "0.8.16" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" 243 | dependencies = [ 244 | "cfg-if", 245 | ] 246 | 247 | [[package]] 248 | name = "crossterm" 249 | version = "0.27.0" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" 252 | dependencies = [ 253 | "bitflags 2.4.0", 254 | "crossterm_winapi", 255 | "libc", 256 | "parking_lot", 257 | "winapi", 258 | ] 259 | 260 | [[package]] 261 | name = "crossterm_winapi" 262 | version = "0.9.1" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 265 | dependencies = [ 266 | "winapi", 267 | ] 268 | 269 | [[package]] 270 | name = "ctrlc" 271 | version = "3.4.4" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "672465ae37dc1bc6380a6547a8883d5dd397b0f1faaad4f265726cc7042a5345" 274 | dependencies = [ 275 | "nix 0.28.0", 276 | "windows-sys 0.52.0", 277 | ] 278 | 279 | [[package]] 280 | name = "directories" 281 | version = "5.0.1" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" 284 | dependencies = [ 285 | "dirs-sys", 286 | ] 287 | 288 | [[package]] 289 | name = "dirs-sys" 290 | version = "0.4.1" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" 293 | dependencies = [ 294 | "libc", 295 | "option-ext", 296 | "redox_users", 297 | "windows-sys 0.48.0", 298 | ] 299 | 300 | [[package]] 301 | name = "either" 302 | version = "1.9.0" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" 305 | 306 | [[package]] 307 | name = "encode_unicode" 308 | version = "0.3.6" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" 311 | 312 | [[package]] 313 | name = "equivalent" 314 | version = "1.0.1" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 317 | 318 | [[package]] 319 | name = "errno" 320 | version = "0.3.9" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" 323 | dependencies = [ 324 | "libc", 325 | "windows-sys 0.52.0", 326 | ] 327 | 328 | [[package]] 329 | name = "fastrand" 330 | version = "2.1.0" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" 333 | 334 | [[package]] 335 | name = "getrandom" 336 | version = "0.2.10" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" 339 | dependencies = [ 340 | "cfg-if", 341 | "libc", 342 | "wasi", 343 | ] 344 | 345 | [[package]] 346 | name = "gimli" 347 | version = "0.28.0" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" 350 | 351 | [[package]] 352 | name = "hashbrown" 353 | version = "0.14.0" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" 356 | 357 | [[package]] 358 | name = "heck" 359 | version = "0.4.1" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 362 | 363 | [[package]] 364 | name = "heck" 365 | version = "0.5.0" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 368 | 369 | [[package]] 370 | name = "home" 371 | version = "0.5.9" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" 374 | dependencies = [ 375 | "windows-sys 0.52.0", 376 | ] 377 | 378 | [[package]] 379 | name = "human-panic" 380 | version = "2.0.0" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "a4c5d0e9120f6bca6120d142c7ede1ba376dd6bf276d69dd3dbe6cbeb7824179" 383 | dependencies = [ 384 | "anstream", 385 | "anstyle", 386 | "backtrace", 387 | "os_info", 388 | "serde", 389 | "serde_derive", 390 | "toml", 391 | "uuid", 392 | ] 393 | 394 | [[package]] 395 | name = "human-sort" 396 | version = "0.2.2" 397 | source = "registry+https://github.com/rust-lang/crates.io-index" 398 | checksum = "140a09c9305e6d5e557e2ed7cbc68e05765a7d4213975b87cb04920689cc6219" 399 | 400 | [[package]] 401 | name = "indexmap" 402 | version = "2.0.0" 403 | source = "registry+https://github.com/rust-lang/crates.io-index" 404 | checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" 405 | dependencies = [ 406 | "equivalent", 407 | "hashbrown", 408 | ] 409 | 410 | [[package]] 411 | name = "indicatif" 412 | version = "0.17.8" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3" 415 | dependencies = [ 416 | "console", 417 | "instant", 418 | "number_prefix", 419 | "portable-atomic", 420 | "rayon", 421 | "unicode-width", 422 | ] 423 | 424 | [[package]] 425 | name = "instant" 426 | version = "0.1.12" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 429 | dependencies = [ 430 | "cfg-if", 431 | ] 432 | 433 | [[package]] 434 | name = "is_executable" 435 | version = "1.0.1" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "fa9acdc6d67b75e626ad644734e8bc6df893d9cd2a834129065d3dd6158ea9c8" 438 | dependencies = [ 439 | "winapi", 440 | ] 441 | 442 | [[package]] 443 | name = "is_terminal_polyfill" 444 | version = "1.70.0" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" 447 | 448 | [[package]] 449 | name = "lazy_static" 450 | version = "1.4.0" 451 | source = "registry+https://github.com/rust-lang/crates.io-index" 452 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 453 | 454 | [[package]] 455 | name = "libc" 456 | version = "0.2.154" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" 459 | 460 | [[package]] 461 | name = "linux-raw-sys" 462 | version = "0.4.13" 463 | source = "registry+https://github.com/rust-lang/crates.io-index" 464 | checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" 465 | 466 | [[package]] 467 | name = "lock_api" 468 | version = "0.4.11" 469 | source = "registry+https://github.com/rust-lang/crates.io-index" 470 | checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" 471 | dependencies = [ 472 | "autocfg", 473 | "scopeguard", 474 | ] 475 | 476 | [[package]] 477 | name = "log" 478 | version = "0.4.20" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 481 | 482 | [[package]] 483 | name = "memchr" 484 | version = "2.6.3" 485 | source = "registry+https://github.com/rust-lang/crates.io-index" 486 | checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" 487 | 488 | [[package]] 489 | name = "memfile" 490 | version = "0.3.2" 491 | source = "registry+https://github.com/rust-lang/crates.io-index" 492 | checksum = "f64636fdb65a5f0740f920c4281f3dbb76a71e25e25914b6d27000739897d40e" 493 | dependencies = [ 494 | "libc", 495 | ] 496 | 497 | [[package]] 498 | name = "memoffset" 499 | version = "0.9.0" 500 | source = "registry+https://github.com/rust-lang/crates.io-index" 501 | checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" 502 | dependencies = [ 503 | "autocfg", 504 | ] 505 | 506 | [[package]] 507 | name = "miniz_oxide" 508 | version = "0.7.1" 509 | source = "registry+https://github.com/rust-lang/crates.io-index" 510 | checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" 511 | dependencies = [ 512 | "adler", 513 | ] 514 | 515 | [[package]] 516 | name = "nix" 517 | version = "0.27.1" 518 | source = "registry+https://github.com/rust-lang/crates.io-index" 519 | checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" 520 | dependencies = [ 521 | "bitflags 2.4.0", 522 | "cfg-if", 523 | "libc", 524 | ] 525 | 526 | [[package]] 527 | name = "nix" 528 | version = "0.28.0" 529 | source = "registry+https://github.com/rust-lang/crates.io-index" 530 | checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" 531 | dependencies = [ 532 | "bitflags 2.4.0", 533 | "cfg-if", 534 | "cfg_aliases", 535 | "libc", 536 | ] 537 | 538 | [[package]] 539 | name = "number_prefix" 540 | version = "0.4.0" 541 | source = "registry+https://github.com/rust-lang/crates.io-index" 542 | checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" 543 | 544 | [[package]] 545 | name = "object" 546 | version = "0.32.1" 547 | source = "registry+https://github.com/rust-lang/crates.io-index" 548 | checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" 549 | dependencies = [ 550 | "memchr", 551 | ] 552 | 553 | [[package]] 554 | name = "option-ext" 555 | version = "0.2.0" 556 | source = "registry+https://github.com/rust-lang/crates.io-index" 557 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 558 | 559 | [[package]] 560 | name = "os_info" 561 | version = "3.7.0" 562 | source = "registry+https://github.com/rust-lang/crates.io-index" 563 | checksum = "006e42d5b888366f1880eda20371fedde764ed2213dc8496f49622fa0c99cd5e" 564 | dependencies = [ 565 | "log", 566 | "serde", 567 | "winapi", 568 | ] 569 | 570 | [[package]] 571 | name = "parking_lot" 572 | version = "0.12.1" 573 | source = "registry+https://github.com/rust-lang/crates.io-index" 574 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 575 | dependencies = [ 576 | "lock_api", 577 | "parking_lot_core", 578 | ] 579 | 580 | [[package]] 581 | name = "parking_lot_core" 582 | version = "0.9.9" 583 | source = "registry+https://github.com/rust-lang/crates.io-index" 584 | checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" 585 | dependencies = [ 586 | "cfg-if", 587 | "libc", 588 | "redox_syscall 0.4.1", 589 | "smallvec", 590 | "windows-targets 0.48.5", 591 | ] 592 | 593 | [[package]] 594 | name = "portable-atomic" 595 | version = "1.4.3" 596 | source = "registry+https://github.com/rust-lang/crates.io-index" 597 | checksum = "31114a898e107c51bb1609ffaf55a0e011cf6a4d7f1170d0015a165082c0338b" 598 | 599 | [[package]] 600 | name = "proc-macro2" 601 | version = "1.0.82" 602 | source = "registry+https://github.com/rust-lang/crates.io-index" 603 | checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b" 604 | dependencies = [ 605 | "unicode-ident", 606 | ] 607 | 608 | [[package]] 609 | name = "quote" 610 | version = "1.0.33" 611 | source = "registry+https://github.com/rust-lang/crates.io-index" 612 | checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" 613 | dependencies = [ 614 | "proc-macro2", 615 | ] 616 | 617 | [[package]] 618 | name = "rayon" 619 | version = "1.10.0" 620 | source = "registry+https://github.com/rust-lang/crates.io-index" 621 | checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" 622 | dependencies = [ 623 | "either", 624 | "rayon-core", 625 | ] 626 | 627 | [[package]] 628 | name = "rayon-core" 629 | version = "1.12.1" 630 | source = "registry+https://github.com/rust-lang/crates.io-index" 631 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 632 | dependencies = [ 633 | "crossbeam-deque", 634 | "crossbeam-utils", 635 | ] 636 | 637 | [[package]] 638 | name = "redox_syscall" 639 | version = "0.2.16" 640 | source = "registry+https://github.com/rust-lang/crates.io-index" 641 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 642 | dependencies = [ 643 | "bitflags 1.3.2", 644 | ] 645 | 646 | [[package]] 647 | name = "redox_syscall" 648 | version = "0.4.1" 649 | source = "registry+https://github.com/rust-lang/crates.io-index" 650 | checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" 651 | dependencies = [ 652 | "bitflags 1.3.2", 653 | ] 654 | 655 | [[package]] 656 | name = "redox_users" 657 | version = "0.4.3" 658 | source = "registry+https://github.com/rust-lang/crates.io-index" 659 | checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" 660 | dependencies = [ 661 | "getrandom", 662 | "redox_syscall 0.2.16", 663 | "thiserror", 664 | ] 665 | 666 | [[package]] 667 | name = "rustc-demangle" 668 | version = "0.1.23" 669 | source = "registry+https://github.com/rust-lang/crates.io-index" 670 | checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" 671 | 672 | [[package]] 673 | name = "rustix" 674 | version = "0.38.34" 675 | source = "registry+https://github.com/rust-lang/crates.io-index" 676 | checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" 677 | dependencies = [ 678 | "bitflags 2.4.0", 679 | "errno", 680 | "libc", 681 | "linux-raw-sys", 682 | "windows-sys 0.52.0", 683 | ] 684 | 685 | [[package]] 686 | name = "rustversion" 687 | version = "1.0.14" 688 | source = "registry+https://github.com/rust-lang/crates.io-index" 689 | checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" 690 | 691 | [[package]] 692 | name = "scopeguard" 693 | version = "1.2.0" 694 | source = "registry+https://github.com/rust-lang/crates.io-index" 695 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 696 | 697 | [[package]] 698 | name = "serde" 699 | version = "1.0.188" 700 | source = "registry+https://github.com/rust-lang/crates.io-index" 701 | checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" 702 | dependencies = [ 703 | "serde_derive", 704 | ] 705 | 706 | [[package]] 707 | name = "serde_derive" 708 | version = "1.0.188" 709 | source = "registry+https://github.com/rust-lang/crates.io-index" 710 | checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" 711 | dependencies = [ 712 | "proc-macro2", 713 | "quote", 714 | "syn", 715 | ] 716 | 717 | [[package]] 718 | name = "serde_spanned" 719 | version = "0.6.4" 720 | source = "registry+https://github.com/rust-lang/crates.io-index" 721 | checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80" 722 | dependencies = [ 723 | "serde", 724 | ] 725 | 726 | [[package]] 727 | name = "smallvec" 728 | version = "1.11.2" 729 | source = "registry+https://github.com/rust-lang/crates.io-index" 730 | checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" 731 | 732 | [[package]] 733 | name = "strsim" 734 | version = "0.11.1" 735 | source = "registry+https://github.com/rust-lang/crates.io-index" 736 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 737 | 738 | [[package]] 739 | name = "strum" 740 | version = "0.26.2" 741 | source = "registry+https://github.com/rust-lang/crates.io-index" 742 | checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" 743 | 744 | [[package]] 745 | name = "strum_macros" 746 | version = "0.26.2" 747 | source = "registry+https://github.com/rust-lang/crates.io-index" 748 | checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" 749 | dependencies = [ 750 | "heck 0.4.1", 751 | "proc-macro2", 752 | "quote", 753 | "rustversion", 754 | "syn", 755 | ] 756 | 757 | [[package]] 758 | name = "syn" 759 | version = "2.0.37" 760 | source = "registry+https://github.com/rust-lang/crates.io-index" 761 | checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" 762 | dependencies = [ 763 | "proc-macro2", 764 | "quote", 765 | "unicode-ident", 766 | ] 767 | 768 | [[package]] 769 | name = "tempfile" 770 | version = "3.10.1" 771 | source = "registry+https://github.com/rust-lang/crates.io-index" 772 | checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" 773 | dependencies = [ 774 | "cfg-if", 775 | "fastrand", 776 | "rustix", 777 | "windows-sys 0.52.0", 778 | ] 779 | 780 | [[package]] 781 | name = "terminal_size" 782 | version = "0.3.0" 783 | source = "registry+https://github.com/rust-lang/crates.io-index" 784 | checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" 785 | dependencies = [ 786 | "rustix", 787 | "windows-sys 0.48.0", 788 | ] 789 | 790 | [[package]] 791 | name = "thiserror" 792 | version = "1.0.48" 793 | source = "registry+https://github.com/rust-lang/crates.io-index" 794 | checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" 795 | dependencies = [ 796 | "thiserror-impl", 797 | ] 798 | 799 | [[package]] 800 | name = "thiserror-impl" 801 | version = "1.0.48" 802 | source = "registry+https://github.com/rust-lang/crates.io-index" 803 | checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" 804 | dependencies = [ 805 | "proc-macro2", 806 | "quote", 807 | "syn", 808 | ] 809 | 810 | [[package]] 811 | name = "toml" 812 | version = "0.8.8" 813 | source = "registry+https://github.com/rust-lang/crates.io-index" 814 | checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" 815 | dependencies = [ 816 | "serde", 817 | "serde_spanned", 818 | "toml_datetime", 819 | "toml_edit", 820 | ] 821 | 822 | [[package]] 823 | name = "toml_datetime" 824 | version = "0.6.5" 825 | source = "registry+https://github.com/rust-lang/crates.io-index" 826 | checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" 827 | dependencies = [ 828 | "serde", 829 | ] 830 | 831 | [[package]] 832 | name = "toml_edit" 833 | version = "0.21.0" 834 | source = "registry+https://github.com/rust-lang/crates.io-index" 835 | checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" 836 | dependencies = [ 837 | "indexmap", 838 | "serde", 839 | "serde_spanned", 840 | "toml_datetime", 841 | ] 842 | 843 | [[package]] 844 | name = "toster" 845 | version = "1.2.1" 846 | dependencies = [ 847 | "clap", 848 | "colored", 849 | "comfy-table", 850 | "command-fds", 851 | "ctrlc", 852 | "directories", 853 | "human-panic", 854 | "human-sort", 855 | "indicatif", 856 | "is_executable", 857 | "memfile", 858 | "rayon", 859 | "tempfile", 860 | "terminal_size", 861 | "wait-timeout", 862 | "which", 863 | ] 864 | 865 | [[package]] 866 | name = "unicode-ident" 867 | version = "1.0.12" 868 | source = "registry+https://github.com/rust-lang/crates.io-index" 869 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 870 | 871 | [[package]] 872 | name = "unicode-width" 873 | version = "0.1.11" 874 | source = "registry+https://github.com/rust-lang/crates.io-index" 875 | checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" 876 | 877 | [[package]] 878 | name = "utf8parse" 879 | version = "0.2.1" 880 | source = "registry+https://github.com/rust-lang/crates.io-index" 881 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 882 | 883 | [[package]] 884 | name = "uuid" 885 | version = "1.4.1" 886 | source = "registry+https://github.com/rust-lang/crates.io-index" 887 | checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" 888 | dependencies = [ 889 | "getrandom", 890 | ] 891 | 892 | [[package]] 893 | name = "wait-timeout" 894 | version = "0.2.0" 895 | source = "registry+https://github.com/rust-lang/crates.io-index" 896 | checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" 897 | dependencies = [ 898 | "libc", 899 | ] 900 | 901 | [[package]] 902 | name = "wasi" 903 | version = "0.11.0+wasi-snapshot-preview1" 904 | source = "registry+https://github.com/rust-lang/crates.io-index" 905 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 906 | 907 | [[package]] 908 | name = "which" 909 | version = "6.0.1" 910 | source = "registry+https://github.com/rust-lang/crates.io-index" 911 | checksum = "8211e4f58a2b2805adfbefbc07bab82958fc91e3836339b1ab7ae32465dce0d7" 912 | dependencies = [ 913 | "either", 914 | "home", 915 | "rustix", 916 | "winsafe", 917 | ] 918 | 919 | [[package]] 920 | name = "winapi" 921 | version = "0.3.9" 922 | source = "registry+https://github.com/rust-lang/crates.io-index" 923 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 924 | dependencies = [ 925 | "winapi-i686-pc-windows-gnu", 926 | "winapi-x86_64-pc-windows-gnu", 927 | ] 928 | 929 | [[package]] 930 | name = "winapi-i686-pc-windows-gnu" 931 | version = "0.4.0" 932 | source = "registry+https://github.com/rust-lang/crates.io-index" 933 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 934 | 935 | [[package]] 936 | name = "winapi-x86_64-pc-windows-gnu" 937 | version = "0.4.0" 938 | source = "registry+https://github.com/rust-lang/crates.io-index" 939 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 940 | 941 | [[package]] 942 | name = "windows-sys" 943 | version = "0.45.0" 944 | source = "registry+https://github.com/rust-lang/crates.io-index" 945 | checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" 946 | dependencies = [ 947 | "windows-targets 0.42.2", 948 | ] 949 | 950 | [[package]] 951 | name = "windows-sys" 952 | version = "0.48.0" 953 | source = "registry+https://github.com/rust-lang/crates.io-index" 954 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 955 | dependencies = [ 956 | "windows-targets 0.48.5", 957 | ] 958 | 959 | [[package]] 960 | name = "windows-sys" 961 | version = "0.52.0" 962 | source = "registry+https://github.com/rust-lang/crates.io-index" 963 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 964 | dependencies = [ 965 | "windows-targets 0.52.5", 966 | ] 967 | 968 | [[package]] 969 | name = "windows-targets" 970 | version = "0.42.2" 971 | source = "registry+https://github.com/rust-lang/crates.io-index" 972 | checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" 973 | dependencies = [ 974 | "windows_aarch64_gnullvm 0.42.2", 975 | "windows_aarch64_msvc 0.42.2", 976 | "windows_i686_gnu 0.42.2", 977 | "windows_i686_msvc 0.42.2", 978 | "windows_x86_64_gnu 0.42.2", 979 | "windows_x86_64_gnullvm 0.42.2", 980 | "windows_x86_64_msvc 0.42.2", 981 | ] 982 | 983 | [[package]] 984 | name = "windows-targets" 985 | version = "0.48.5" 986 | source = "registry+https://github.com/rust-lang/crates.io-index" 987 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 988 | dependencies = [ 989 | "windows_aarch64_gnullvm 0.48.5", 990 | "windows_aarch64_msvc 0.48.5", 991 | "windows_i686_gnu 0.48.5", 992 | "windows_i686_msvc 0.48.5", 993 | "windows_x86_64_gnu 0.48.5", 994 | "windows_x86_64_gnullvm 0.48.5", 995 | "windows_x86_64_msvc 0.48.5", 996 | ] 997 | 998 | [[package]] 999 | name = "windows-targets" 1000 | version = "0.52.5" 1001 | source = "registry+https://github.com/rust-lang/crates.io-index" 1002 | checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" 1003 | dependencies = [ 1004 | "windows_aarch64_gnullvm 0.52.5", 1005 | "windows_aarch64_msvc 0.52.5", 1006 | "windows_i686_gnu 0.52.5", 1007 | "windows_i686_gnullvm", 1008 | "windows_i686_msvc 0.52.5", 1009 | "windows_x86_64_gnu 0.52.5", 1010 | "windows_x86_64_gnullvm 0.52.5", 1011 | "windows_x86_64_msvc 0.52.5", 1012 | ] 1013 | 1014 | [[package]] 1015 | name = "windows_aarch64_gnullvm" 1016 | version = "0.42.2" 1017 | source = "registry+https://github.com/rust-lang/crates.io-index" 1018 | checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" 1019 | 1020 | [[package]] 1021 | name = "windows_aarch64_gnullvm" 1022 | version = "0.48.5" 1023 | source = "registry+https://github.com/rust-lang/crates.io-index" 1024 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1025 | 1026 | [[package]] 1027 | name = "windows_aarch64_gnullvm" 1028 | version = "0.52.5" 1029 | source = "registry+https://github.com/rust-lang/crates.io-index" 1030 | checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" 1031 | 1032 | [[package]] 1033 | name = "windows_aarch64_msvc" 1034 | version = "0.42.2" 1035 | source = "registry+https://github.com/rust-lang/crates.io-index" 1036 | checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" 1037 | 1038 | [[package]] 1039 | name = "windows_aarch64_msvc" 1040 | version = "0.48.5" 1041 | source = "registry+https://github.com/rust-lang/crates.io-index" 1042 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1043 | 1044 | [[package]] 1045 | name = "windows_aarch64_msvc" 1046 | version = "0.52.5" 1047 | source = "registry+https://github.com/rust-lang/crates.io-index" 1048 | checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" 1049 | 1050 | [[package]] 1051 | name = "windows_i686_gnu" 1052 | version = "0.42.2" 1053 | source = "registry+https://github.com/rust-lang/crates.io-index" 1054 | checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" 1055 | 1056 | [[package]] 1057 | name = "windows_i686_gnu" 1058 | version = "0.48.5" 1059 | source = "registry+https://github.com/rust-lang/crates.io-index" 1060 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1061 | 1062 | [[package]] 1063 | name = "windows_i686_gnu" 1064 | version = "0.52.5" 1065 | source = "registry+https://github.com/rust-lang/crates.io-index" 1066 | checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" 1067 | 1068 | [[package]] 1069 | name = "windows_i686_gnullvm" 1070 | version = "0.52.5" 1071 | source = "registry+https://github.com/rust-lang/crates.io-index" 1072 | checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" 1073 | 1074 | [[package]] 1075 | name = "windows_i686_msvc" 1076 | version = "0.42.2" 1077 | source = "registry+https://github.com/rust-lang/crates.io-index" 1078 | checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" 1079 | 1080 | [[package]] 1081 | name = "windows_i686_msvc" 1082 | version = "0.48.5" 1083 | source = "registry+https://github.com/rust-lang/crates.io-index" 1084 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1085 | 1086 | [[package]] 1087 | name = "windows_i686_msvc" 1088 | version = "0.52.5" 1089 | source = "registry+https://github.com/rust-lang/crates.io-index" 1090 | checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" 1091 | 1092 | [[package]] 1093 | name = "windows_x86_64_gnu" 1094 | version = "0.42.2" 1095 | source = "registry+https://github.com/rust-lang/crates.io-index" 1096 | checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" 1097 | 1098 | [[package]] 1099 | name = "windows_x86_64_gnu" 1100 | version = "0.48.5" 1101 | source = "registry+https://github.com/rust-lang/crates.io-index" 1102 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1103 | 1104 | [[package]] 1105 | name = "windows_x86_64_gnu" 1106 | version = "0.52.5" 1107 | source = "registry+https://github.com/rust-lang/crates.io-index" 1108 | checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" 1109 | 1110 | [[package]] 1111 | name = "windows_x86_64_gnullvm" 1112 | version = "0.42.2" 1113 | source = "registry+https://github.com/rust-lang/crates.io-index" 1114 | checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" 1115 | 1116 | [[package]] 1117 | name = "windows_x86_64_gnullvm" 1118 | version = "0.48.5" 1119 | source = "registry+https://github.com/rust-lang/crates.io-index" 1120 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1121 | 1122 | [[package]] 1123 | name = "windows_x86_64_gnullvm" 1124 | version = "0.52.5" 1125 | source = "registry+https://github.com/rust-lang/crates.io-index" 1126 | checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" 1127 | 1128 | [[package]] 1129 | name = "windows_x86_64_msvc" 1130 | version = "0.42.2" 1131 | source = "registry+https://github.com/rust-lang/crates.io-index" 1132 | checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" 1133 | 1134 | [[package]] 1135 | name = "windows_x86_64_msvc" 1136 | version = "0.48.5" 1137 | source = "registry+https://github.com/rust-lang/crates.io-index" 1138 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1139 | 1140 | [[package]] 1141 | name = "windows_x86_64_msvc" 1142 | version = "0.52.5" 1143 | source = "registry+https://github.com/rust-lang/crates.io-index" 1144 | checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" 1145 | 1146 | [[package]] 1147 | name = "winsafe" 1148 | version = "0.0.19" 1149 | source = "registry+https://github.com/rust-lang/crates.io-index" 1150 | checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" 1151 | --------------------------------------------------------------------------------