From a7196deb982b912d7cdbb22373ec756910b53ed3 Mon Sep 17 00:00:00 2001
From: aecsocket <43144841+aecsocket@users.noreply.github.com>
Date: Tue, 25 Aug 2026 13:57:14 +0100
Subject: [PATCH] cel #define preprocessor, regex ext
---
.../moderation/technical-review/rules.vue | 24 +-
.../internal/moderation/tech_review/rules.rs | 2 +-
.../moderation/tech_review/rules_scan.rs | 2 +-
apps/labrinth/src/util/cel.rs | 413 ++++++++++++++++++
apps/labrinth/src/util/mod.rs | 1 +
5 files changed, 439 insertions(+), 3 deletions(-)
create mode 100644 apps/labrinth/src/util/cel.rs
diff --git a/apps/frontend/src/pages/moderation/technical-review/rules.vue b/apps/frontend/src/pages/moderation/technical-review/rules.vue
index 5c73571168..95ca2931c5 100644
--- a/apps/frontend/src/pages/moderation/technical-review/rules.vue
+++ b/apps/frontend/src/pages/moderation/technical-review/rules.vue
@@ -98,6 +98,26 @@
{{ ruleOutputSchemaText }}
+
+ Extensions
+
+
+ #define ISSUE_TYPE "OBFUSCATED_NAMES"
+
+trace.issue_type == ISSUE_TYPE
@@ -486,7 +506,9 @@ import type { Component } from 'vue'
import IssueDetailPath from '~/components/ui/moderation/IssueDetailPath.vue'
-const DEFAULT_RULE = `trace.issue_type == "OBFUSCATED_NAMES"
+const DEFAULT_RULE = `#define ISSUE_TYPE "OBFUSCATED_NAMES"
+
+trace.issue_type == ISSUE_TYPE
? "low"
: null`
const RULE_EDITOR_OPTIONS: Partial = {
diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review/rules.rs b/apps/labrinth/src/routes/internal/moderation/tech_review/rules.rs
index ff5ec950d3..c9beebef34 100644
--- a/apps/labrinth/src/routes/internal/moderation/tech_review/rules.rs
+++ b/apps/labrinth/src/routes/internal/moderation/tech_review/rules.rs
@@ -22,7 +22,7 @@ use crate::{
},
queue::session::AuthQueue,
routes::ApiError,
- util::{error::Context, validate::validation_errors_to_string},
+ util::{cel, error::Context, validate::validation_errors_to_string},
};
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review/rules_scan.rs b/apps/labrinth/src/routes/internal/moderation/tech_review/rules_scan.rs
index a8b3ed3766..aa5da3d68b 100644
--- a/apps/labrinth/src/routes/internal/moderation/tech_review/rules_scan.rs
+++ b/apps/labrinth/src/routes/internal/moderation/tech_review/rules_scan.rs
@@ -28,7 +28,7 @@ use crate::{
models::pats::Scopes,
queue::session::AuthQueue,
routes::ApiError,
- util::error::Context,
+ util::{cel, error::Context},
};
const RULE_SCAN_LOCK_ID: i64 = 0x6465_6c70_6869_7275;
diff --git a/apps/labrinth/src/util/cel.rs b/apps/labrinth/src/util/cel.rs
new file mode 100644
index 0000000000..eb1eda7db3
--- /dev/null
+++ b/apps/labrinth/src/util/cel.rs
@@ -0,0 +1,413 @@
+use std::collections::BTreeMap;
+
+pub use ::cel::Context;
+use ::cel::{ExecutionError, Program as CelProgram, Value};
+use chumsky::{Parser, prelude::*};
+use eyre::{Result, WrapErr, eyre};
+use thiserror::Error;
+
+const MAX_PREPROCESSED_SIZE: usize = 1_048_576;
+
+pub struct Program {
+ inner: CelProgram,
+}
+
+impl Program {
+ pub fn compile(source: &str) -> Result {
+ let source = preprocess(source)
+ .wrap_err("failed to preprocess cel expression")?;
+ let inner =
+ CelProgram::compile(&source).map_err(|error| eyre!(error))?;
+
+ Ok(Self { inner })
+ }
+
+ pub fn execute<'a>(
+ &self,
+ context: &Context<'a>,
+ ) -> std::result::Result {
+ self.inner.execute(context)
+ }
+}
+
+#[derive(Debug)]
+struct Definition {
+ name: String,
+ replacement: String,
+}
+
+#[derive(Debug, Error)]
+enum PreprocessorError {
+ #[error("invalid `#define` on line {line}, column {column}: {message}")]
+ InvalidDefinition {
+ line: usize,
+ column: usize,
+ message: String,
+ },
+ #[error("macro `{name}` is defined more than once")]
+ DuplicateDefinition { name: String },
+ #[error("recursive macro expansion: {path}")]
+ RecursiveExpansion { path: String },
+ #[error("preprocessed expression exceeds {MAX_PREPROCESSED_SIZE} bytes")]
+ ExpressionTooLarge,
+}
+
+#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
+enum LexState {
+ #[default]
+ Normal,
+ String {
+ quote: u8,
+ triple: bool,
+ raw: bool,
+ },
+ BlockComment,
+}
+
+fn definition_parser() -> impl Parser> {
+ let horizontal_whitespace = one_of(" \t").repeated();
+ let identifier = filter(|character: &char| {
+ character.is_ascii_alphabetic() || *character == '_'
+ })
+ .then(
+ filter(|character: &char| {
+ character.is_ascii_alphanumeric() || *character == '_'
+ })
+ .repeated(),
+ )
+ .map(|(first, rest)| {
+ std::iter::once(first).chain(rest).collect::()
+ });
+ let replacement = any().repeated().at_least(1).collect::();
+
+ horizontal_whitespace
+ .clone()
+ .ignore_then(just('#'))
+ .then_ignore(horizontal_whitespace.clone())
+ .then_ignore(just("define"))
+ .then_ignore(horizontal_whitespace.clone().at_least(1))
+ .ignore_then(identifier)
+ .then_ignore(horizontal_whitespace.at_least(1))
+ .then(replacement)
+ .map(|(name, replacement)| Definition {
+ name,
+ replacement: replacement.trim().to_string(),
+ })
+ .then_ignore(end())
+}
+
+fn preprocess(source: &str) -> Result {
+ let mut definitions = BTreeMap::new();
+ let mut expression = String::with_capacity(source.len());
+ let mut state = LexState::Normal;
+
+ for (line_index, line) in source.split_inclusive('\n').enumerate() {
+ let directive = state == LexState::Normal
+ && line.trim_start_matches([' ', '\t']).starts_with('#');
+
+ if directive {
+ let directive = line.trim_end_matches(['\r', '\n']);
+ let definition =
+ definition_parser().parse(directive).map_err(|errors| {
+ let error =
+ errors.into_iter().next().unwrap_or_else(|| {
+ Simple::custom(0..0, "invalid directive")
+ });
+ PreprocessorError::InvalidDefinition {
+ line: line_index + 1,
+ column: error.span().start + 1,
+ message: error.to_string(),
+ }
+ })?;
+
+ if definitions
+ .insert(definition.name.clone(), definition.replacement)
+ .is_some()
+ {
+ return Err(PreprocessorError::DuplicateDefinition {
+ name: definition.name,
+ });
+ }
+
+ if line.ends_with('\n') {
+ expression.push('\n');
+ }
+ } else {
+ expression.push_str(line);
+ advance_lex_state(line, &mut state);
+ }
+ }
+
+ let mut expanded = String::with_capacity(expression.len());
+ expand(&expression, &definitions, &mut Vec::new(), &mut expanded)?;
+ Ok(expanded)
+}
+
+fn expand(
+ source: &str,
+ definitions: &BTreeMap,
+ stack: &mut Vec,
+ output: &mut String,
+) -> Result<(), PreprocessorError> {
+ let bytes = source.as_bytes();
+ let mut index = 0;
+ let mut state = LexState::Normal;
+
+ while index < bytes.len() {
+ match state {
+ LexState::Normal => {
+ if bytes[index..].starts_with(b"//") {
+ push(output, "//")?;
+ index += 2;
+ while index < bytes.len() {
+ let character = source[index..]
+ .chars()
+ .next()
+ .expect("the index is within the string");
+ let mut encoded = [0; 4];
+ push(output, character.encode_utf8(&mut encoded))?;
+ index += character.len_utf8();
+ if character == '\n' {
+ break;
+ }
+ }
+ continue;
+ }
+
+ if bytes[index..].starts_with(b"/*") {
+ push(output, "/*")?;
+ index += 2;
+ state = LexState::BlockComment;
+ continue;
+ }
+
+ if let Some((length, string_state)) = string_start(bytes, index)
+ {
+ push(output, &source[index..index + length])?;
+ index += length;
+ state = string_state;
+ continue;
+ }
+
+ if bytes[index].is_ascii_alphabetic() || bytes[index] == b'_' {
+ let start = index;
+ index += 1;
+ while index < bytes.len()
+ && (bytes[index].is_ascii_alphanumeric()
+ || bytes[index] == b'_')
+ {
+ index += 1;
+ }
+
+ let identifier = &source[start..index];
+ if let Some(replacement) = definitions.get(identifier) {
+ if let Some(cycle_start) =
+ stack.iter().position(|name| name == identifier)
+ {
+ let mut path = stack[cycle_start..].to_vec();
+ path.push(identifier.to_string());
+ return Err(
+ PreprocessorError::RecursiveExpansion {
+ path: path.join(" -> "),
+ },
+ );
+ }
+
+ stack.push(identifier.to_string());
+ expand(replacement, definitions, stack, output)?;
+ stack.pop();
+ } else {
+ push(output, identifier)?;
+ }
+ continue;
+ }
+ }
+ LexState::String { quote, triple, raw } => {
+ if !raw && bytes[index] == b'\\' {
+ let end = (index + 2).min(bytes.len());
+ push(output, &source[index..end])?;
+ index = end;
+ continue;
+ }
+
+ let closing_length = if triple { 3 } else { 1 };
+ if bytes[index] == quote
+ && bytes[index..].len() >= closing_length
+ && bytes[index..index + closing_length]
+ .iter()
+ .all(|character| *character == quote)
+ {
+ push(output, &source[index..index + closing_length])?;
+ index += closing_length;
+ state = LexState::Normal;
+ continue;
+ }
+ }
+ LexState::BlockComment => {
+ if bytes[index..].starts_with(b"*/") {
+ push(output, "*/")?;
+ index += 2;
+ state = LexState::Normal;
+ continue;
+ }
+ }
+ }
+
+ let character = source[index..]
+ .chars()
+ .next()
+ .expect("the index is within the string");
+ let mut encoded = [0; 4];
+ push(output, character.encode_utf8(&mut encoded))?;
+ index += character.len_utf8();
+ }
+
+ Ok(())
+}
+
+fn push(output: &mut String, value: &str) -> Result<(), PreprocessorError> {
+ if output.len().saturating_add(value.len()) > MAX_PREPROCESSED_SIZE {
+ return Err(PreprocessorError::ExpressionTooLarge);
+ }
+ output.push_str(value);
+ Ok(())
+}
+
+fn advance_lex_state(source: &str, state: &mut LexState) {
+ let bytes = source.as_bytes();
+ let mut index = 0;
+
+ while index < bytes.len() {
+ match *state {
+ LexState::Normal => {
+ if bytes[index..].starts_with(b"//") {
+ return;
+ }
+ if bytes[index..].starts_with(b"/*") {
+ *state = LexState::BlockComment;
+ index += 2;
+ continue;
+ }
+ if let Some((length, string_state)) = string_start(bytes, index)
+ {
+ *state = string_state;
+ index += length;
+ continue;
+ }
+ }
+ LexState::String { quote, triple, raw } => {
+ if !raw && bytes[index] == b'\\' {
+ index = (index + 2).min(bytes.len());
+ continue;
+ }
+
+ let closing_length = if triple { 3 } else { 1 };
+ if bytes[index] == quote
+ && bytes[index..].len() >= closing_length
+ && bytes[index..index + closing_length]
+ .iter()
+ .all(|character| *character == quote)
+ {
+ *state = LexState::Normal;
+ index += closing_length;
+ continue;
+ }
+ }
+ LexState::BlockComment => {
+ if bytes[index..].starts_with(b"*/") {
+ *state = LexState::Normal;
+ index += 2;
+ continue;
+ }
+ }
+ }
+
+ index += source[index..]
+ .chars()
+ .next()
+ .expect("the index is within the string")
+ .len_utf8();
+ }
+
+ if matches!(state, LexState::String { triple: false, .. }) {
+ *state = LexState::Normal;
+ }
+}
+
+fn string_start(bytes: &[u8], index: usize) -> Option<(usize, LexState)> {
+ let remaining = &bytes[index..];
+ let (prefix_length, raw) = if remaining.len() >= 3
+ && matches!(remaining[0].to_ascii_lowercase(), b'b' | b'r')
+ && matches!(remaining[1].to_ascii_lowercase(), b'b' | b'r')
+ && remaining[0].to_ascii_lowercase()
+ != remaining[1].to_ascii_lowercase()
+ {
+ (2, true)
+ } else if remaining.len() >= 2
+ && matches!(remaining[0].to_ascii_lowercase(), b'b' | b'r')
+ {
+ (1, remaining[0].eq_ignore_ascii_case(&b'r'))
+ } else {
+ (0, false)
+ };
+
+ let quote = *remaining.get(prefix_length)?;
+ if !matches!(quote, b'\'' | b'"') {
+ return None;
+ }
+
+ let triple =
+ remaining
+ .get(prefix_length..prefix_length + 3)
+ .is_some_and(|quotes| {
+ quotes.iter().all(|character| *character == quote)
+ });
+ let quote_length = if triple { 3 } else { 1 };
+
+ Some((
+ prefix_length + quote_length,
+ LexState::String { quote, triple, raw },
+ ))
+}
+
+#[cfg(test)]
+mod tests {
+ use super::preprocess;
+
+ #[test]
+ fn expands_object_macros() {
+ let source =
+ "#define ISSUE OBFUSCATED_NAMES\ntrace.issue_type == ISSUE";
+ assert_eq!(
+ preprocess(source).unwrap(),
+ "\ntrace.issue_type == OBFUSCATED_NAMES"
+ );
+ }
+
+ #[test]
+ fn recursively_expands_macros() {
+ let source =
+ "#define RESULT SEVERITY\n#define SEVERITY \"low\"\nRESULT";
+ assert_eq!(preprocess(source).unwrap(), "\n\n\"low\"");
+ }
+
+ #[test]
+ fn does_not_expand_strings_or_comments() {
+ let source =
+ "#define VALUE expanded\nVALUE == \"VALUE\" // VALUE\n/* VALUE */";
+ assert_eq!(
+ preprocess(source).unwrap(),
+ "\nexpanded == \"VALUE\" // VALUE\n/* VALUE */"
+ );
+ }
+
+ #[test]
+ fn reports_recursive_macros() {
+ let source = "#define FIRST SECOND\n#define SECOND FIRST\nFIRST";
+ let error = preprocess(source).unwrap_err();
+ assert_eq!(
+ error.to_string(),
+ "recursive macro expansion: FIRST -> SECOND -> FIRST"
+ );
+ }
+}
diff --git a/apps/labrinth/src/util/mod.rs b/apps/labrinth/src/util/mod.rs
index dc17c4eeb6..7a9b0de91c 100644
--- a/apps/labrinth/src/util/mod.rs
+++ b/apps/labrinth/src/util/mod.rs
@@ -4,6 +4,7 @@ pub mod archon;
pub mod avalara1099;
pub mod bitflag;
pub mod captcha;
+pub mod cel;
pub mod cors;
pub mod date;
pub mod error;