diff --git a/Cargo.lock b/Cargo.lock index d11f002..3ac07b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1123,6 +1123,7 @@ dependencies = [ "futures", "prost", "prost-types", + "rsjudge-utils", "tokio", "tokio-stream", "tonic", @@ -1136,6 +1137,7 @@ version = "0.1.0" dependencies = [ "async-trait", "futures", + "rsjudge-utils", "temp-dir", "tokio", ] @@ -1156,10 +1158,18 @@ dependencies = [ "caps", "nix", "once_cell", + "rsjudge-utils", "rustc_version", "uzers", ] +[[package]] +name = "rsjudge-utils" +version = "0.1.0" +dependencies = [ + "anyhow", +] + [[package]] name = "rustc-demangle" version = "0.1.23" diff --git a/Cargo.toml b/Cargo.toml index 6ca66b8..02e86d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,12 @@ [workspace] -members = ["crates/rsjudge-grpc", "crates/rsjudge-judger", "crates/rsjudge-rest", "crates/rsjudge-runner", "xtask"] +members = [ + "crates/rsjudge-grpc", + "crates/rsjudge-judger", + "crates/rsjudge-rest", + "crates/rsjudge-runner", + "crates/rsjudge-utils", + "xtask", +] [workspace.package] version = "0.1.0" diff --git a/crates/rsjudge-grpc/Cargo.toml b/crates/rsjudge-grpc/Cargo.toml index a698f7d..b8e4857 100644 --- a/crates/rsjudge-grpc/Cargo.toml +++ b/crates/rsjudge-grpc/Cargo.toml @@ -18,4 +18,5 @@ tonic-buf-build = "0.2.0" [build-dependencies] anyhow = "1.0.81" +rsjudge-utils = { version = "0.1.0", path = "../rsjudge-utils" } tonic-build = "0.11.0" diff --git a/crates/rsjudge-grpc/build.rs b/crates/rsjudge-grpc/build.rs index 45e522c..2ebf0cd 100644 --- a/crates/rsjudge-grpc/build.rs +++ b/crates/rsjudge-grpc/build.rs @@ -1,5 +1,6 @@ use std::{env, io::BufRead, path::PathBuf, process::Command}; +use rsjudge_utils::command::check_output; use tonic_build::configure; /// Generate Rust code from the proto files. @@ -15,32 +16,18 @@ fn main() -> anyhow::Result<()> { out_dir }; - let buf_ls_files = Command::new("buf") - .current_dir("proto") - .arg("ls-files") - .output()?; - - assert!( - buf_ls_files.status.success(), - "buf ls-files failed with: {:#?}", - buf_ls_files - ); + let buf_ls_files = check_output(Command::new("buf").current_dir("proto").arg("ls-files"))?; let protos = buf_ls_files .stdout .lines() .filter_map(|line| line.ok().filter(|s| !s.is_empty())); - let buf_export = Command::new("buf") - .args(["export", "proto", "-o"]) - .arg(&proto_out_dir) - .output()?; - - assert!( - buf_export.status.success(), - "buf export failed with: {:#?}", - buf_export - ); + check_output( + Command::new("buf") + .args(["export", "proto", "-o"]) + .arg(&proto_out_dir), + )?; for proto in protos { configure() @@ -53,3 +40,15 @@ fn main() -> anyhow::Result<()> { Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_display_cmd() { + let mut cmd = Command::new("echo"); + cmd.arg("hello"); + assert_eq!(display_cmd(&cmd), r#""echo" "hello""#); + } +} diff --git a/crates/rsjudge-judger/Cargo.toml b/crates/rsjudge-judger/Cargo.toml index c36a9cb..8e0c992 100644 --- a/crates/rsjudge-judger/Cargo.toml +++ b/crates/rsjudge-judger/Cargo.toml @@ -9,6 +9,7 @@ rust-version.workspace = true [dependencies] async-trait = "0.1.78" futures = "0.3.30" +rsjudge-utils = { version = "0.1.0", path = "../rsjudge-utils" } tokio = { version = "1.36.0", features = ["io-util", "fs", "macros"] } [dev-dependencies] diff --git a/crates/rsjudge-judger/src/comparer/default_comparer.rs b/crates/rsjudge-judger/src/comparer/default_comparer.rs index 810121d..d722c12 100644 --- a/crates/rsjudge-judger/src/comparer/default_comparer.rs +++ b/crates/rsjudge-judger/src/comparer/default_comparer.rs @@ -4,9 +4,10 @@ use std::io; use async_trait::async_trait; use futures::try_join; +use rsjudge_utils::trim::trim_ascii_end; use tokio::io::{AsyncBufReadExt as _, AsyncRead, BufReader}; -use crate::{utils::trim::slice::trim_ascii_end, CompareResult, Comparer}; +use crate::{CompareResult, Comparer}; /// A default comparer implementation with basic configurations. pub struct DefaultComparer { diff --git a/crates/rsjudge-judger/src/comparer/mod.rs b/crates/rsjudge-judger/src/comparer/mod.rs index df5a89d..9376842 100644 --- a/crates/rsjudge-judger/src/comparer/mod.rs +++ b/crates/rsjudge-judger/src/comparer/mod.rs @@ -12,7 +12,6 @@ pub enum CompareResult { PresentationError, } -// TODO: Migrate to AsyncComparer trait #[async_trait] pub trait Comparer { async fn compare(&self, out: Out, ans: Ans) -> io::Result diff --git a/crates/rsjudge-judger/src/lib.rs b/crates/rsjudge-judger/src/lib.rs index 500385f..19e251e 100644 --- a/crates/rsjudge-judger/src/lib.rs +++ b/crates/rsjudge-judger/src/lib.rs @@ -1,4 +1,3 @@ pub mod comparer; -mod utils; pub use comparer::{default_comparer::DefaultComparer, CompareResult, Comparer}; diff --git a/crates/rsjudge-judger/src/utils/mod.rs b/crates/rsjudge-judger/src/utils/mod.rs deleted file mode 100644 index 5bf3591..0000000 --- a/crates/rsjudge-judger/src/utils/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub(crate) mod trim; diff --git a/crates/rsjudge-judger/src/utils/trim.rs b/crates/rsjudge-judger/src/utils/trim.rs deleted file mode 100644 index 1686d52..0000000 --- a/crates/rsjudge-judger/src/utils/trim.rs +++ /dev/null @@ -1 +0,0 @@ -pub(crate) mod slice; diff --git a/crates/rsjudge-judger/src/utils/trim/slice.rs b/crates/rsjudge-judger/src/utils/trim/slice.rs deleted file mode 100644 index 91cc5de..0000000 --- a/crates/rsjudge-judger/src/utils/trim/slice.rs +++ /dev/null @@ -1,34 +0,0 @@ -#![allow(dead_code)] - -#[inline] -pub(crate) const fn trim_ascii_start(mut bytes: &[u8]) -> &[u8] { - // Note: A pattern matching based approach (instead of indexing) allows - // making the function const. - while let [first, rest @ ..] = bytes { - if first.is_ascii_whitespace() { - bytes = rest; - } else { - break; - } - } - bytes -} - -#[inline] -pub(crate) const fn trim_ascii_end(mut bytes: &[u8]) -> &[u8] { - // Note: A pattern matching based approach (instead of indexing) allows - // making the function const. - while let [rest @ .., last] = bytes { - if last.is_ascii_whitespace() { - bytes = rest; - } else { - break; - } - } - bytes -} - -#[inline] -pub(crate) const fn trim_ascii(bytes: &[u8]) -> &[u8] { - trim_ascii_end(trim_ascii_start(bytes)) -} diff --git a/crates/rsjudge-runner/Cargo.toml b/crates/rsjudge-runner/Cargo.toml index 7f52d26..0254e55 100644 --- a/crates/rsjudge-runner/Cargo.toml +++ b/crates/rsjudge-runner/Cargo.toml @@ -16,3 +16,6 @@ uzers = "0.11.3" [build-dependencies] rustc_version = "0.4.0" + +[dev-dependencies] +rsjudge-utils = { path = "../rsjudge-utils" } diff --git a/crates/rsjudge-runner/examples/exploit.rs b/crates/rsjudge-runner/examples/exploit.rs index a923860..66e8573 100644 --- a/crates/rsjudge-runner/examples/exploit.rs +++ b/crates/rsjudge-runner/examples/exploit.rs @@ -1,7 +1,9 @@ use std::{path::PathBuf, process::Command}; +use anyhow::anyhow; use caps::{has_cap, read, CapSet, Capability}; use rsjudge_runner::{user::builder, RunAs}; +use rsjudge_utils::command::check_output; /// An attempt to exploit the runner by running a binary with a setuid call. /// @@ -20,13 +22,19 @@ fn main() -> anyhow::Result<()> { dbg!(read(None, CapSet::Inheritable).unwrap()); dbg!(read(None, CapSet::Permitted).unwrap()); - if !has_cap(None, CapSet::Permitted, Capability::CAP_SETUID)? - || !has_cap(None, CapSet::Permitted, Capability::CAP_SETGID)? - || !has_cap(None, CapSet::Permitted, Capability::CAP_DAC_READ_SEARCH)? - { - eprintln!("Not all required capabilities are set, exiting."); - return Err(anyhow::anyhow!("Missing required capabilities.")); - } + [ + Capability::CAP_SETUID, + Capability::CAP_SETGID, + Capability::CAP_DAC_READ_SEARCH, + ] + .into_iter() + .try_for_each(|cap| { + if has_cap(None, CapSet::Effective, cap)? { + Ok(()) + } else { + Err(anyhow!("Missing capability: {:?}", cap)) + } + })?; // Get the path to the examples. // This crate is located at crates/rsjudge-runner, @@ -39,16 +47,12 @@ fn main() -> anyhow::Result<()> { let exploit_inner = examples.join("exploit_inner"); - let status = Command::new(dbg!(exploit_inner)) - .run_as(builder()?) - .output()?; - assert!(status.status.success()); + let status = check_output(Command::new(dbg!(exploit_inner)).run_as(builder()?))?; println!("{}", String::from_utf8_lossy(&status.stdout)); println!("{}", String::from_utf8_lossy(&status.stderr)); let normal = examples.join("normal"); - let status = Command::new(normal).run_as(builder()?).output()?; - assert!(status.status.success()); + let status = check_output(Command::new(normal).run_as(builder()?))?; println!("{}", String::from_utf8_lossy(&status.stdout)); println!("{}", String::from_utf8_lossy(&status.stderr)); diff --git a/crates/rsjudge-utils/Cargo.toml b/crates/rsjudge-utils/Cargo.toml new file mode 100644 index 0000000..9e55852 --- /dev/null +++ b/crates/rsjudge-utils/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "rsjudge-utils" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0.81" diff --git a/crates/rsjudge-utils/src/command.rs b/crates/rsjudge-utils/src/command.rs new file mode 100644 index 0000000..d06a2c3 --- /dev/null +++ b/crates/rsjudge-utils/src/command.rs @@ -0,0 +1,39 @@ +use std::{ + io::ErrorKind, + process::{Command, Output}, +}; + +use anyhow::{bail, ensure}; + +/// Display a command in a human-readable format, suitable for error messages. +pub fn display_cmd(cmd: &Command) -> String { + let mut s = format!("{:?}", cmd.get_program().to_string_lossy()); + s.extend( + cmd.get_args() + .map(|arg| format!(" {:?}", arg.to_string_lossy())), + ); + s +} + +/// Run a command, returning the output if succeeded, with some error handling. +pub fn check_output(cmd: &mut Command) -> anyhow::Result { + let output = match cmd.output() { + Ok(o) => o, + Err(e) => match e.kind() { + ErrorKind::NotFound => bail!( + "`{}` not found. Please install it or check your PATH.", + cmd.get_program().to_string_lossy() + ), + _ => Err(e)?, + }, + }; + + ensure!( + output.status.success(), + "`{:?}` failed with: {:#?}", + display_cmd(cmd), + output + ); + + Ok(output) +} diff --git a/crates/rsjudge-utils/src/lib.rs b/crates/rsjudge-utils/src/lib.rs new file mode 100644 index 0000000..a621fce --- /dev/null +++ b/crates/rsjudge-utils/src/lib.rs @@ -0,0 +1,9 @@ +//! A collection of utility functions for the rsjudge project. + +#![warn(missing_docs)] + +/// Functions for trimming ASCII whitespace from `[u8]` and `str`. +pub mod trim; + +/// Functions for working with `std::process::Command`. +pub mod command; diff --git a/crates/rsjudge-utils/src/trim.rs b/crates/rsjudge-utils/src/trim.rs new file mode 100644 index 0000000..3468ec4 --- /dev/null +++ b/crates/rsjudge-utils/src/trim.rs @@ -0,0 +1,72 @@ +/// Returns a byte slice with leading ASCII whitespace bytes removed. +/// +/// 'Whitespace' refers to the definition used by +/// `u8::is_ascii_whitespace`. +/// +/// # Examples +/// +/// ``` +/// use rsjudge_utils::trim::trim_ascii_start; +/// assert_eq!(trim_ascii_start(b" \t hello world\n"), b"hello world\n"); +/// assert_eq!(trim_ascii_start(b" "), b""); +/// assert_eq!(trim_ascii_start(b""), b""); +/// ``` +#[inline] +pub const fn trim_ascii_start(mut bytes: &[u8]) -> &[u8] { + // Note: A pattern matching based approach (instead of indexing) allows + // making the function const. + while let [first, rest @ ..] = bytes { + if first.is_ascii_whitespace() { + bytes = rest; + } else { + break; + } + } + bytes +} + +/// Returns a byte slice with trailing ASCII whitespace bytes removed. +/// +/// 'Whitespace' refers to the definition used by +/// `u8::is_ascii_whitespace`. +/// +/// # Examples +/// +/// ``` +/// use rsjudge_utils::trim::trim_ascii_end; +/// assert_eq!(trim_ascii_end(b"\r hello world\n "), b"\r hello world"); +/// assert_eq!(trim_ascii_end(b" "), b""); +/// assert_eq!(trim_ascii_end(b""), b""); +/// ``` +#[inline] +pub const fn trim_ascii_end(mut bytes: &[u8]) -> &[u8] { + // Note: A pattern matching based approach (instead of indexing) allows + // making the function const. + while let [rest @ .., last] = bytes { + if last.is_ascii_whitespace() { + bytes = rest; + } else { + break; + } + } + bytes +} + +/// Returns a byte slice with leading and trailing ASCII whitespace bytes +/// removed. +/// +/// 'Whitespace' refers to the definition used by +/// `u8::is_ascii_whitespace`. +/// +/// # Examples +/// +/// ``` +/// use rsjudge_utils::trim::trim_ascii; +/// assert_eq!(trim_ascii(b"\r hello world\n "), b"hello world"); +/// assert_eq!(trim_ascii(b" "), b""); +/// assert_eq!(trim_ascii(b""), b""); +/// ``` +#[inline] +pub const fn trim_ascii(bytes: &[u8]) -> &[u8] { + trim_ascii_end(trim_ascii_start(bytes)) +}