gameplay: port add-filter command and queued kick
Browse files- crates/urt-gameplay/src/bin/urt-filter-commands.rs +131 -0
- crates/urt-gameplay/src/filter_commands.rs +268 -0
- crates/urt-gameplay/src/lib.rs +2 -0
- docs/FINDINGS.md +2 -0
- docs/PROVISIONAL_TODO.md +2 -0
- docs/RESEARCH_LEDGER.md +4 -0
- docs/REVERSE_ENGINEERING_TODO.md +2 -0
- docs/research/README.md +2 -0
- docs/research/RUST_FILTER_ADD_COMMAND.md +5 -0
- docs/research/rust-filter-add-command.json +0 -0
- docs/research/rust-health-impulse.json +1 -1
- scripts/analyze_rust_filter_add_command.py +17 -0
- scripts/test_rust_filter_add_command.py +9 -0
- xtask/checks.tsv +20 -0
crates/urt-gameplay/src/bin/urt-filter-commands.rs
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
use urt_gameplay::filter_commands::*;
|
| 2 |
+
use urt_gameplay::native_policy::FilterRecord;
|
| 3 |
+
fn hex(v: &[u8]) -> String {
|
| 4 |
+
v.iter().map(|v| format!("{v:02x}")).collect()
|
| 5 |
+
}
|
| 6 |
+
fn blob(w: &[u32], p: &mut usize) -> Vec<u8> {
|
| 7 |
+
let n = w[*p] as usize;
|
| 8 |
+
*p += 1;
|
| 9 |
+
let b = w[*p..*p + n].iter().map(|v| *v as u8).collect();
|
| 10 |
+
*p += n;
|
| 11 |
+
b
|
| 12 |
+
}
|
| 13 |
+
fn row_json(r: FilterRecord) -> String {
|
| 14 |
+
format!("[{},{},{}]", r.mask, r.comparison, r.metadata)
|
| 15 |
+
}
|
| 16 |
+
fn diagnostics(d: &[CommandDiagnostic]) -> String {
|
| 17 |
+
d.iter()
|
| 18 |
+
.map(|d| match d {
|
| 19 |
+
CommandDiagnostic::Full => r#"{"op":"full"}"#.into(),
|
| 20 |
+
CommandDiagnostic::Removed => r#"{"op":"removed"}"#.into(),
|
| 21 |
+
CommandDiagnostic::BadAddress(t) => {
|
| 22 |
+
format!(r#"{{"op":"bad-address","text":"{}"}}"#, hex(t))
|
| 23 |
+
}
|
| 24 |
+
CommandDiagnostic::NotFound(t) => {
|
| 25 |
+
format!(r#"{{"op":"not-found","text":"{}"}}"#, hex(t))
|
| 26 |
+
}
|
| 27 |
+
})
|
| 28 |
+
.collect::<Vec<String>>()
|
| 29 |
+
.join(",")
|
| 30 |
+
}
|
| 31 |
+
fn children_json(children: &[CommandChild]) -> String {
|
| 32 |
+
children
|
| 33 |
+
.iter()
|
| 34 |
+
.map(|p| {
|
| 35 |
+
format!(
|
| 36 |
+
r#"{{"result":{},"diagnostics":[{}],"exports":[{}]}}"#,
|
| 37 |
+
u8::from(p.result),
|
| 38 |
+
diagnostics(&p.diagnostics),
|
| 39 |
+
if p.export { "252500" } else { "" }
|
| 40 |
+
)
|
| 41 |
+
})
|
| 42 |
+
.collect::<Vec<_>>()
|
| 43 |
+
.join(",")
|
| 44 |
+
}
|
| 45 |
+
fn command_json(
|
| 46 |
+
p: FilterCommand,
|
| 47 |
+
rows: &[FilterRecord],
|
| 48 |
+
count: usize,
|
| 49 |
+
ring: u32,
|
| 50 |
+
selector: u32,
|
| 51 |
+
) -> String {
|
| 52 |
+
let events = p
|
| 53 |
+
.effects
|
| 54 |
+
.iter()
|
| 55 |
+
.map(|e| match e {
|
| 56 |
+
FilterCommandEffect::Console(t) => format!(r#"{{"op":"console","text":"{}"}}"#, hex(t)),
|
| 57 |
+
FilterCommandEffect::Send { recipient, text } => format!(
|
| 58 |
+
r#"{{"op":"send","recipient":{},"text":"{}"}}"#,
|
| 59 |
+
recipient,
|
| 60 |
+
hex(text)
|
| 61 |
+
),
|
| 62 |
+
FilterCommandEffect::Userinfo(slot) => {
|
| 63 |
+
format!(r#"{{"op":"userinfo","slot":{}}}"#, slot)
|
| 64 |
+
}
|
| 65 |
+
FilterCommandEffect::Insert { text, save } => format!(
|
| 66 |
+
r#"{{"op":"insert","text":"{}","save":{}}}"#,
|
| 67 |
+
hex(text),
|
| 68 |
+
save
|
| 69 |
+
),
|
| 70 |
+
FilterCommandEffect::Remove(t) => format!(r#"{{"op":"remove","text":"{}"}}"#, hex(t)),
|
| 71 |
+
FilterCommandEffect::Enqueue { mode, text } => format!(
|
| 72 |
+
r#"{{"op":"enqueue","mode":{},"text":"{}"}}"#,
|
| 73 |
+
mode,
|
| 74 |
+
hex(text)
|
| 75 |
+
),
|
| 76 |
+
})
|
| 77 |
+
.collect::<Vec<_>>()
|
| 78 |
+
.join(",");
|
| 79 |
+
format!(
|
| 80 |
+
r#"{{"events":[{}],"children":[{}],"ring":{},"selector":{},"count":{},"rows":[{}]}}"#,
|
| 81 |
+
events,
|
| 82 |
+
children_json(&p.children),
|
| 83 |
+
ring,
|
| 84 |
+
selector,
|
| 85 |
+
count,
|
| 86 |
+
rows.iter()
|
| 87 |
+
.copied()
|
| 88 |
+
.map(row_json)
|
| 89 |
+
.collect::<Vec<_>>()
|
| 90 |
+
.join(",")
|
| 91 |
+
)
|
| 92 |
+
}
|
| 93 |
+
fn main() {
|
| 94 |
+
let mut args = std::env::args().skip(1);
|
| 95 |
+
let kind = args.next().unwrap();
|
| 96 |
+
let w: Vec<u32> = args.map(|s| s.parse().unwrap()).collect();
|
| 97 |
+
match kind.as_str() {
|
| 98 |
+
"add" => {
|
| 99 |
+
let mut p = 2;
|
| 100 |
+
let text = blob(&w, &mut p);
|
| 101 |
+
let userinfo = blob(&w, &mut p);
|
| 102 |
+
assert_eq!(p, w.len());
|
| 103 |
+
let mut rows = vec![
|
| 104 |
+
FilterRecord {
|
| 105 |
+
mask: 0x11223344,
|
| 106 |
+
comparison: u32::MAX,
|
| 107 |
+
metadata: 0x99aabbcc
|
| 108 |
+
};
|
| 109 |
+
8
|
| 110 |
+
];
|
| 111 |
+
let mut count = 0;
|
| 112 |
+
let mut ring = u32::MAX;
|
| 113 |
+
let mut selector = 0;
|
| 114 |
+
let issuer = if w[0] == u32::MAX {
|
| 115 |
+
None
|
| 116 |
+
} else {
|
| 117 |
+
Some(w[0] as i32)
|
| 118 |
+
};
|
| 119 |
+
let input = FilterAddInput {
|
| 120 |
+
text: &text,
|
| 121 |
+
issuer,
|
| 122 |
+
active: &[w[1] as i32; 64],
|
| 123 |
+
userinfo: &userinfo,
|
| 124 |
+
};
|
| 125 |
+
let out =
|
| 126 |
+
add_filter_command(&mut rows, &mut count, input, &mut ring, &mut selector).unwrap();
|
| 127 |
+
println!("{}", command_json(out, &rows, count, ring, selector));
|
| 128 |
+
}
|
| 129 |
+
_ => panic!("unknown policy"),
|
| 130 |
+
}
|
| 131 |
+
}
|
crates/urt-gameplay/src/filter_commands.rs
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! Recovered filter command/caller policies; native effects remain explicit.
|
| 2 |
+
use crate::filter_lifecycle::{FilterMutation, FilterNotice};
|
| 3 |
+
use crate::native_policy::{
|
| 4 |
+
Error, FilterDiagnostic, FilterInsert, FilterRecord, info_lookup, insert_filter, signed_prefix,
|
| 5 |
+
};
|
| 6 |
+
#[derive(Clone, Debug, PartialEq, Eq)]
|
| 7 |
+
pub enum FilterCommandEffect {
|
| 8 |
+
Console(Vec<u8>),
|
| 9 |
+
Send { recipient: i32, text: Vec<u8> },
|
| 10 |
+
Userinfo(i32),
|
| 11 |
+
Insert { text: Vec<u8>, save: i32 },
|
| 12 |
+
Remove(Vec<u8>),
|
| 13 |
+
Enqueue { mode: i32, text: Vec<u8> },
|
| 14 |
+
}
|
| 15 |
+
#[derive(Clone, Debug, PartialEq, Eq)]
|
| 16 |
+
pub enum CommandDiagnostic {
|
| 17 |
+
Full,
|
| 18 |
+
BadAddress(Vec<u8>),
|
| 19 |
+
NotFound(Vec<u8>),
|
| 20 |
+
Removed,
|
| 21 |
+
}
|
| 22 |
+
#[derive(Clone, Debug, PartialEq, Eq)]
|
| 23 |
+
pub struct CommandChild {
|
| 24 |
+
pub result: bool,
|
| 25 |
+
pub diagnostics: Vec<CommandDiagnostic>,
|
| 26 |
+
pub export: bool,
|
| 27 |
+
}
|
| 28 |
+
impl From<FilterInsert> for CommandChild {
|
| 29 |
+
fn from(p: FilterInsert) -> Self {
|
| 30 |
+
Self {
|
| 31 |
+
result: p.result,
|
| 32 |
+
export: p.export,
|
| 33 |
+
diagnostics: p
|
| 34 |
+
.diagnostics
|
| 35 |
+
.into_iter()
|
| 36 |
+
.map(|d| match d {
|
| 37 |
+
FilterDiagnostic::Full => CommandDiagnostic::Full,
|
| 38 |
+
FilterDiagnostic::BadAddress(t) => CommandDiagnostic::BadAddress(t),
|
| 39 |
+
})
|
| 40 |
+
.collect(),
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
}
|
| 44 |
+
impl From<FilterMutation> for CommandChild {
|
| 45 |
+
fn from(p: FilterMutation) -> Self {
|
| 46 |
+
Self {
|
| 47 |
+
result: p.result,
|
| 48 |
+
export: p.export,
|
| 49 |
+
diagnostics: p
|
| 50 |
+
.notices
|
| 51 |
+
.into_iter()
|
| 52 |
+
.map(|d| match d {
|
| 53 |
+
FilterNotice::Removed => CommandDiagnostic::Removed,
|
| 54 |
+
FilterNotice::BadAddress(t) => CommandDiagnostic::BadAddress(t),
|
| 55 |
+
FilterNotice::NotFound(t) => CommandDiagnostic::NotFound(t),
|
| 56 |
+
})
|
| 57 |
+
.collect(),
|
| 58 |
+
}
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
#[derive(Clone, Debug, PartialEq, Eq)]
|
| 62 |
+
pub struct FilterCommand {
|
| 63 |
+
pub effects: Vec<FilterCommandEffect>,
|
| 64 |
+
pub children: Vec<CommandChild>,
|
| 65 |
+
}
|
| 66 |
+
fn cbytes(v: &[u8]) -> &[u8] {
|
| 67 |
+
v.split(|b| *b == 0).next().unwrap_or_default()
|
| 68 |
+
}
|
| 69 |
+
fn format(
|
| 70 |
+
template: &[u8],
|
| 71 |
+
args: &[crate::qvm_format::Argument<'_>],
|
| 72 |
+
ring: &mut u32,
|
| 73 |
+
) -> Result<Vec<u8>, Error> {
|
| 74 |
+
let mut text = crate::qvm_format::render(template, args).map_err(|_| Error::InvalidInput)?;
|
| 75 |
+
text.pop();
|
| 76 |
+
*ring = ring.wrapping_add(1);
|
| 77 |
+
Ok(text)
|
| 78 |
+
}
|
| 79 |
+
fn usage(issuer: Option<i32>, add: bool, ring: &mut u32) -> Result<FilterCommandEffect, Error> {
|
| 80 |
+
if let Some(recipient) = issuer {
|
| 81 |
+
let template = if add {
|
| 82 |
+
b"chat \"^5[rcon] ^7Usage: /addip <ip-mask>\"".as_slice()
|
| 83 |
+
} else {
|
| 84 |
+
b"chat \"^5[rcon] ^7Usage: /removeip <ip-mask>\"".as_slice()
|
| 85 |
+
};
|
| 86 |
+
Ok(FilterCommandEffect::Send {
|
| 87 |
+
recipient,
|
| 88 |
+
text: format(template, &[], ring)?,
|
| 89 |
+
})
|
| 90 |
+
} else {
|
| 91 |
+
Ok(FilterCommandEffect::Console(if add {
|
| 92 |
+
b"Usage: addip <ip-mask>\n".to_vec()
|
| 93 |
+
} else {
|
| 94 |
+
b"Usage: removeip <ip-mask>\n".to_vec()
|
| 95 |
+
}))
|
| 96 |
+
}
|
| 97 |
+
}
|
| 98 |
+
pub struct FilterAddInput<'a> {
|
| 99 |
+
pub text: &'a [u8],
|
| 100 |
+
pub issuer: Option<i32>,
|
| 101 |
+
pub active: &'a [i32; 64],
|
| 102 |
+
pub userinfo: &'a [u8],
|
| 103 |
+
}
|
| 104 |
+
/// Original253100: length<3 means client number, not necessarily numeric input.
|
| 105 |
+
/// Active clients always queue a kick after insertion, including parse failures.
|
| 106 |
+
pub fn add_filter_command(
|
| 107 |
+
records: &mut [FilterRecord],
|
| 108 |
+
count: &mut usize,
|
| 109 |
+
input: FilterAddInput<'_>,
|
| 110 |
+
ring: &mut u32,
|
| 111 |
+
selector: &mut u32,
|
| 112 |
+
) -> Result<FilterCommand, Error> {
|
| 113 |
+
if records.len() > 4096 || *count > records.len() {
|
| 114 |
+
return Err(Error::InvalidCount);
|
| 115 |
+
}
|
| 116 |
+
if input.issuer.is_some_and(|v| !(0..64).contains(&v)) {
|
| 117 |
+
return Err(Error::InvalidIndex);
|
| 118 |
+
}
|
| 119 |
+
let text = cbytes(input.text);
|
| 120 |
+
if text.len() > 1024 {
|
| 121 |
+
return Err(Error::InvalidInput);
|
| 122 |
+
}
|
| 123 |
+
let mut staged = records.to_vec();
|
| 124 |
+
let mut next = *count;
|
| 125 |
+
let mut counter = *ring;
|
| 126 |
+
let mut info = *selector;
|
| 127 |
+
let mut effects = vec![];
|
| 128 |
+
let mut children = vec![];
|
| 129 |
+
if text.is_empty() {
|
| 130 |
+
effects.push(usage(input.issuer, true, &mut counter)?);
|
| 131 |
+
} else {
|
| 132 |
+
let mut client = None;
|
| 133 |
+
let address;
|
| 134 |
+
let ip = if text.len() < 3 {
|
| 135 |
+
let slot = signed_prefix(text);
|
| 136 |
+
if !(0..64).contains(&slot) || input.active[slot as usize] == 0 {
|
| 137 |
+
return Ok(FilterCommand { effects, children });
|
| 138 |
+
}
|
| 139 |
+
client = Some(slot);
|
| 140 |
+
effects.push(FilterCommandEffect::Userinfo(slot));
|
| 141 |
+
let raw = cbytes(input.userinfo);
|
| 142 |
+
let raw = &raw[..raw.len().min(1023)];
|
| 143 |
+
address = info_lookup(Some(raw), Some(b"ip"), &mut info)?.value;
|
| 144 |
+
address.as_slice()
|
| 145 |
+
} else {
|
| 146 |
+
text
|
| 147 |
+
};
|
| 148 |
+
effects.push(FilterCommandEffect::Insert {
|
| 149 |
+
text: ip.to_vec(),
|
| 150 |
+
save: 1,
|
| 151 |
+
});
|
| 152 |
+
children.push(insert_filter(&mut staged, &mut next, ip, 1)?.into());
|
| 153 |
+
if let Some(slot) = client {
|
| 154 |
+
let text = format(
|
| 155 |
+
b"clientkick %d\n",
|
| 156 |
+
&[crate::qvm_format::Argument::Word(slot)],
|
| 157 |
+
&mut counter,
|
| 158 |
+
)?;
|
| 159 |
+
effects.push(FilterCommandEffect::Enqueue { mode: 1, text });
|
| 160 |
+
}
|
| 161 |
+
}
|
| 162 |
+
records.copy_from_slice(&staged);
|
| 163 |
+
*count = next;
|
| 164 |
+
*ring = counter;
|
| 165 |
+
*selector = info;
|
| 166 |
+
Ok(FilterCommand { effects, children })
|
| 167 |
+
}
|
| 168 |
+
#[cfg(test)]
|
| 169 |
+
mod add_tests {
|
| 170 |
+
use super::*;
|
| 171 |
+
fn row() -> FilterRecord {
|
| 172 |
+
FilterRecord {
|
| 173 |
+
mask: 7,
|
| 174 |
+
comparison: 8,
|
| 175 |
+
metadata: 9,
|
| 176 |
+
}
|
| 177 |
+
}
|
| 178 |
+
#[test]
|
| 179 |
+
fn missing_ip_and_full_table_still_queue_kick() {
|
| 180 |
+
let mut rows = [row()];
|
| 181 |
+
let mut count = 0;
|
| 182 |
+
let mut ring = u32::MAX;
|
| 183 |
+
let mut selector = 0;
|
| 184 |
+
let p = add_filter_command(
|
| 185 |
+
&mut rows,
|
| 186 |
+
&mut count,
|
| 187 |
+
FilterAddInput {
|
| 188 |
+
text: b"x",
|
| 189 |
+
issuer: None,
|
| 190 |
+
active: &[1; 64],
|
| 191 |
+
userinfo: b"\\name\\x",
|
| 192 |
+
},
|
| 193 |
+
&mut ring,
|
| 194 |
+
&mut selector,
|
| 195 |
+
)
|
| 196 |
+
.unwrap();
|
| 197 |
+
assert_eq!(count, 1);
|
| 198 |
+
assert_eq!(selector, 1);
|
| 199 |
+
assert_eq!(ring, 0);
|
| 200 |
+
assert!(matches!(
|
| 201 |
+
p.effects.last(),
|
| 202 |
+
Some(FilterCommandEffect::Enqueue { mode: 1, .. })
|
| 203 |
+
));
|
| 204 |
+
assert!(!p.children[0].diagnostics.is_empty());
|
| 205 |
+
let mut rows = vec![row(); 4096];
|
| 206 |
+
count = 4096;
|
| 207 |
+
let p = add_filter_command(
|
| 208 |
+
&mut rows,
|
| 209 |
+
&mut count,
|
| 210 |
+
FilterAddInput {
|
| 211 |
+
text: b"0",
|
| 212 |
+
issuer: None,
|
| 213 |
+
active: &[1; 64],
|
| 214 |
+
userinfo: b"\\ip\\1.2.3.4",
|
| 215 |
+
},
|
| 216 |
+
&mut ring,
|
| 217 |
+
&mut selector,
|
| 218 |
+
)
|
| 219 |
+
.unwrap();
|
| 220 |
+
assert!(!p.children[0].result);
|
| 221 |
+
assert!(!p.children[0].export);
|
| 222 |
+
assert!(matches!(
|
| 223 |
+
p.effects.last(),
|
| 224 |
+
Some(FilterCommandEffect::Enqueue { .. })
|
| 225 |
+
));
|
| 226 |
+
}
|
| 227 |
+
#[test]
|
| 228 |
+
fn inactive_client_does_nothing_and_unsupported_address_is_atomic() {
|
| 229 |
+
let mut rows = [row()];
|
| 230 |
+
let mut count = 0;
|
| 231 |
+
let mut ring = 7;
|
| 232 |
+
let mut selector = 0;
|
| 233 |
+
let p = add_filter_command(
|
| 234 |
+
&mut rows,
|
| 235 |
+
&mut count,
|
| 236 |
+
FilterAddInput {
|
| 237 |
+
text: b"63",
|
| 238 |
+
issuer: None,
|
| 239 |
+
active: &[0; 64],
|
| 240 |
+
userinfo: b"",
|
| 241 |
+
},
|
| 242 |
+
&mut ring,
|
| 243 |
+
&mut selector,
|
| 244 |
+
)
|
| 245 |
+
.unwrap();
|
| 246 |
+
assert!(p.effects.is_empty());
|
| 247 |
+
assert_eq!(rows, [row()]);
|
| 248 |
+
assert!(
|
| 249 |
+
add_filter_command(
|
| 250 |
+
&mut rows,
|
| 251 |
+
&mut count,
|
| 252 |
+
FilterAddInput {
|
| 253 |
+
text: &[b'1'; 128],
|
| 254 |
+
issuer: None,
|
| 255 |
+
active: &[1; 64],
|
| 256 |
+
userinfo: b""
|
| 257 |
+
},
|
| 258 |
+
&mut ring,
|
| 259 |
+
&mut selector
|
| 260 |
+
)
|
| 261 |
+
.is_err()
|
| 262 |
+
);
|
| 263 |
+
assert_eq!(rows, [row()]);
|
| 264 |
+
assert_eq!(count, 0);
|
| 265 |
+
assert_eq!(ring, 7);
|
| 266 |
+
assert_eq!(selector, 0);
|
| 267 |
+
}
|
| 268 |
+
}
|
crates/urt-gameplay/src/lib.rs
CHANGED
|
@@ -185,3 +185,5 @@ pub mod native_policy;
|
|
| 185 |
pub mod filter_lifecycle;
|
| 186 |
|
| 187 |
pub mod filter_loading;
|
|
|
|
|
|
|
|
|
| 185 |
pub mod filter_lifecycle;
|
| 186 |
|
| 187 |
pub mod filter_loading;
|
| 188 |
+
|
| 189 |
+
pub mod filter_commands;
|
docs/FINDINGS.md
CHANGED
|
@@ -1841,3 +1841,5 @@ RE-952: Twenty-four actual export/reload compositions transfer captured file byt
|
|
| 1841 |
RE-953: Rust matches24 original export/reload compositions, retaining stale prior files when export is absent, all-hole truncation, wildcard-mask loss, INT_MIN metadata reload as0, duplicate routing and retained-table growth. The staged loader rejects unsupported domains without mutation. Unit tests cover these losses, duplicate growth and atomic unsupported prior-file rejection. Captured write bytes are supplied as read replies; actual filesystem acceptance remains open. [Evidence](research/RUST_FILTER_ROUNDTRIP.md).
|
| 1842 |
|
| 1843 |
RE-954: Ninety-six actual253100 wrappers recover length-under3 client-number routing, active-slot gating, supplied1024-byte userinfo and actual70503 ip lookup, insertion and exact usage/kick formatting. Non-numeric short input can select slot0. A clientkick command is queued even after missing-ip insertion failure. Selector and formatter wraps, invalid slots and direct masks are verified; exports and native effects remain captured. [Evidence](research/QVM_FILTER_ADD_COMMAND.md).
|
|
|
|
|
|
|
|
|
| 1841 |
RE-953: Rust matches24 original export/reload compositions, retaining stale prior files when export is absent, all-hole truncation, wildcard-mask loss, INT_MIN metadata reload as0, duplicate routing and retained-table growth. The staged loader rejects unsupported domains without mutation. Unit tests cover these losses, duplicate growth and atomic unsupported prior-file rejection. Captured write bytes are supplied as read replies; actual filesystem acceptance remains open. [Evidence](research/RUST_FILTER_ROUNDTRIP.md).
|
| 1842 |
|
| 1843 |
RE-954: Ninety-six actual253100 wrappers recover length-under3 client-number routing, active-slot gating, supplied1024-byte userinfo and actual70503 ip lookup, insertion and exact usage/kick formatting. Non-numeric short input can select slot0. A clientkick command is queued even after missing-ip insertion failure. Selector and formatter wraps, invalid slots and direct masks are verified; exports and native effects remain captured. [Evidence](research/QVM_FILTER_ADD_COMMAND.md).
|
| 1844 |
+
|
| 1845 |
+
RE-955: Rust matches96 original add-filter command cases, retaining short-input numeric routing, active-slot/userinfo gates, selector toggles, exact usage/kick text and formatter wrap. Missing IP insertion failures still queue clientkick; an extra unit test verifies full-table refusal also queues a kick. Unsupported domains reject atomically. Native userinfo, queued commands, network and export remain captured plans. [Evidence](research/RUST_FILTER_ADD_COMMAND.md).
|
docs/PROVISIONAL_TODO.md
CHANGED
|
@@ -1891,3 +1891,5 @@ RE-934–943 completed bounded filter export/removal/metadata and insertion/remo
|
|
| 1891 |
RE-944–953 completed bounded time/expiry/loading/duration and export/reload contracts. RE-954 tracks remaining commands/callers, native ownership and runtime acceptance. [Batch and next work](research/QVM_FILTER_LOADING_BATCH.md).
|
| 1892 |
|
| 1893 |
- [x] RE-954 Original add-filter client lookup and queued kick command ([evidence](research/QVM_FILTER_ADD_COMMAND.md)).
|
|
|
|
|
|
|
|
|
| 1891 |
RE-944–953 completed bounded time/expiry/loading/duration and export/reload contracts. RE-954 tracks remaining commands/callers, native ownership and runtime acceptance. [Batch and next work](research/QVM_FILTER_LOADING_BATCH.md).
|
| 1892 |
|
| 1893 |
- [x] RE-954 Original add-filter client lookup and queued kick command ([evidence](research/QVM_FILTER_ADD_COMMAND.md)).
|
| 1894 |
+
|
| 1895 |
+
- [x] RE-955 Rust add-filter wrapper and unconditional queued kick ([evidence](research/RUST_FILTER_ADD_COMMAND.md)).
|
docs/RESEARCH_LEDGER.md
CHANGED
|
@@ -2485,3 +2485,7 @@ Batch RE-944–953 boundary: the single `cargo xtask test-full --since ed9db0e6`
|
|
| 2485 |
## 2026-10-28 — RE-954 Original add-filter client lookup and queued kick command
|
| 2486 |
|
| 2487 |
Ninety-six actual253100 wrappers recover length-under3 client-number routing, active-slot gating, supplied1024-byte userinfo and actual70503 ip lookup, insertion and exact usage/kick formatting. Non-numeric short input can select slot0. A clientkick command is queued even after missing-ip insertion failure. Selector and formatter wraps, invalid slots and direct masks are verified; exports and native effects remain captured. Focused `cargo xtask test-group qvm-filter-add-command` passes with deterministic report regeneration, relevant regressions and storage/documentation/whitespace checks. Full suite deferred to the tenth-commit boundary. Original assets unchanged; native IO effects remain captured.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2485 |
## 2026-10-28 — RE-954 Original add-filter client lookup and queued kick command
|
| 2486 |
|
| 2487 |
Ninety-six actual253100 wrappers recover length-under3 client-number routing, active-slot gating, supplied1024-byte userinfo and actual70503 ip lookup, insertion and exact usage/kick formatting. Non-numeric short input can select slot0. A clientkick command is queued even after missing-ip insertion failure. Selector and formatter wraps, invalid slots and direct masks are verified; exports and native effects remain captured. Focused `cargo xtask test-group qvm-filter-add-command` passes with deterministic report regeneration, relevant regressions and storage/documentation/whitespace checks. Full suite deferred to the tenth-commit boundary. Original assets unchanged; native IO effects remain captured.
|
| 2488 |
+
|
| 2489 |
+
## 2026-10-28 — RE-955 Rust add-filter wrapper and unconditional queued kick
|
| 2490 |
+
|
| 2491 |
+
Rust matches96 original add-filter command cases, retaining short-input numeric routing, active-slot/userinfo gates, selector toggles, exact usage/kick text and formatter wrap. Missing IP insertion failures still queue clientkick; an extra unit test verifies full-table refusal also queues a kick. Unsupported domains reject atomically. Native userinfo, queued commands, network and export remain captured plans. Focused `cargo xtask test-group rust-filter-add-command` passes with deterministic report regeneration, relevant regressions and storage/documentation/whitespace checks. Full suite deferred to the tenth-commit boundary. Original assets unchanged; native IO effects remain captured.
|
docs/REVERSE_ENGINEERING_TODO.md
CHANGED
|
@@ -1928,3 +1928,5 @@ RE-944–953 recover bounded time, expiry, loading, duration and persistence rou
|
|
| 1928 |
- [ ] RE-954 Recover remaining filter commands/callers and native time/file/network ownership, bot setup/userinfo and full Rust/Bevy/Solari acceptance ([next work](research/QVM_FILTER_LOADING_BATCH.md)).
|
| 1929 |
|
| 1930 |
- [x] RE-954 Original add-filter client lookup and queued kick command ([evidence](research/QVM_FILTER_ADD_COMMAND.md)).
|
|
|
|
|
|
|
|
|
| 1928 |
- [ ] RE-954 Recover remaining filter commands/callers and native time/file/network ownership, bot setup/userinfo and full Rust/Bevy/Solari acceptance ([next work](research/QVM_FILTER_LOADING_BATCH.md)).
|
| 1929 |
|
| 1930 |
- [x] RE-954 Original add-filter client lookup and queued kick command ([evidence](research/QVM_FILTER_ADD_COMMAND.md)).
|
| 1931 |
+
|
| 1932 |
+
- [x] RE-955 Rust add-filter wrapper and unconditional queued kick ([evidence](research/RUST_FILTER_ADD_COMMAND.md)).
|
docs/research/README.md
CHANGED
|
@@ -1463,3 +1463,5 @@ The extension collector adds five focused tests for native tail calls, command-r
|
|
| 1463 |
- [RE-944–953 filter loading batch and RE-954 next work](QVM_FILTER_LOADING_BATCH.md).
|
| 1464 |
|
| 1465 |
- [RE-954: Original add-filter client lookup and queued kick command](QVM_FILTER_ADD_COMMAND.md).
|
|
|
|
|
|
|
|
|
| 1463 |
- [RE-944–953 filter loading batch and RE-954 next work](QVM_FILTER_LOADING_BATCH.md).
|
| 1464 |
|
| 1465 |
- [RE-954: Original add-filter client lookup and queued kick command](QVM_FILTER_ADD_COMMAND.md).
|
| 1466 |
+
|
| 1467 |
+
- [RE-955: Rust add-filter wrapper and unconditional queued kick](RUST_FILTER_ADD_COMMAND.md).
|
docs/research/RUST_FILTER_ADD_COMMAND.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Rust add-filter wrapper and unconditional queued kick
|
| 2 |
+
|
| 3 |
+
RE-955. Focused validation: `cargo xtask test-group rust-filter-add-command`. [Machine report](rust-filter-add-command.json).
|
| 4 |
+
|
| 5 |
+
Rust matches96 original add-filter command cases, retaining short-input numeric routing, active-slot/userinfo gates, selector toggles, exact usage/kick text and formatter wrap. Missing IP insertion failures still queue clientkick; an extra unit test verifies full-table refusal also queues a kick. Unsupported domains reject atomically. Native userinfo, queued commands, network and export remain captured plans.
|
docs/research/rust-filter-add-command.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
docs/research/rust-health-impulse.json
CHANGED
|
@@ -2985,7 +2985,7 @@
|
|
| 2985 |
"exit_code": 2
|
| 2986 |
}
|
| 2987 |
],
|
| 2988 |
-
"rust_unit_tests":
|
| 2989 |
"source_report": "docs/research/qvm-health-impulse.json",
|
| 2990 |
"source_report_sha256": "625fd8064004ea6dbdf398d51d0af844d9c635ff04c0a0790886dddb6ab81e71"
|
| 2991 |
}
|
|
|
|
| 2985 |
"exit_code": 2
|
| 2986 |
}
|
| 2987 |
],
|
| 2988 |
+
"rust_unit_tests": 211,
|
| 2989 |
"source_report": "docs/research/qvm-health-impulse.json",
|
| 2990 |
"source_report_sha256": "625fd8064004ea6dbdf398d51d0af844d9c635ff04c0a0790886dddb6ab81e71"
|
| 2991 |
}
|
scripts/analyze_rust_filter_add_command.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Compare recovered Rust policy with original QVM fixtures."""
|
| 2 |
+
import hashlib,json,os,subprocess
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from analyze_qvm import write_json
|
| 5 |
+
from rust_lifecycle_support import result
|
| 6 |
+
|
| 7 |
+
def main():
|
| 8 |
+
subprocess.run(['cargo','build','--offline','--package','urt-gameplay','--bin','urt-filter-commands'],check=True,capture_output=True)
|
| 9 |
+
binary=Path(os.environ.get('CARGO_TARGET_DIR','target'))/'debug'/'urt-filter-commands'
|
| 10 |
+
path=Path('docs/research/qvm-filter-add-command.json');data=path.read_bytes();probes=[]
|
| 11 |
+
for p in json.loads(data)['probes']:
|
| 12 |
+
probes.append(dict(source={k:p[k] for k in ['events', 'children', 'ring', 'selector', 'count', 'rows']},rust=result(binary,'add',encode(p['input']))))
|
| 13 |
+
write_json(Path('docs/research/rust-filter-add-command.json'),dict(source_report=str(path),source_report_sha256=hashlib.sha256(data).hexdigest(),probes=probes,limits='Finite source/Rust contract parity; native time/file/network replies remain supplied or captured.'))
|
| 14 |
+
|
| 15 |
+
def encode(a):
|
| 16 |
+
t=bytes.fromhex(a['text']);u=bytes.fromhex(a['userinfo']);return [a['issuer'],a['active'],len(t),*t,len(u),*u]
|
| 17 |
+
if __name__=='__main__':main()
|
scripts/test_rust_filter_add_command.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import hashlib,json,unittest
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
class PortTests(unittest.TestCase):
|
| 4 |
+
def test_original_parity(self):
|
| 5 |
+
r=json.loads(Path('docs/research/rust-filter-add-command.json').read_text());self.assertGreaterEqual(len(r['probes']),12)
|
| 6 |
+
for p in r['probes']:self.assertEqual(p['source'],p['rust'])
|
| 7 |
+
def test_identity(self):
|
| 8 |
+
r=json.loads(Path('docs/research/rust-filter-add-command.json').read_text());self.assertEqual(hashlib.sha256(Path(r['source_report']).read_bytes()).hexdigest(),r['source_report_sha256'])
|
| 9 |
+
if __name__=='__main__':unittest.main()
|
xtask/checks.tsv
CHANGED
|
@@ -3308,3 +3308,23 @@ report scripts/analyze_qvm_filter_add_command.py docs/research/qvm-filter-add-co
|
|
| 3308 |
edge docs/research/qvm-filter-add-command.json scripts/test_qvm_filter_add_command.py
|
| 3309 |
group qvm-filter-add-command scripts/analyze_qvm_filter_add_command.py
|
| 3310 |
edge scripts/analyze_qvm_filter_add_command.py scripts/analyze_qvm_resumer_audit.py
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3308 |
edge docs/research/qvm-filter-add-command.json scripts/test_qvm_filter_add_command.py
|
| 3309 |
group qvm-filter-add-command scripts/analyze_qvm_filter_add_command.py
|
| 3310 |
edge scripts/analyze_qvm_filter_add_command.py scripts/analyze_qvm_resumer_audit.py
|
| 3311 |
+
report scripts/analyze_rust_filter_add_command.py docs/research/rust-filter-add-command.json
|
| 3312 |
+
edge docs/research/rust-filter-add-command.json scripts/test_rust_filter_add_command.py
|
| 3313 |
+
group rust-filter-add-command scripts/analyze_rust_filter_add_command.py
|
| 3314 |
+
edge scripts/analyze_rust_filter_add_command.py scripts/analyze_qvm_resumer_audit.py
|
| 3315 |
+
edge Cargo.toml scripts/analyze_rust_filter_add_command.py
|
| 3316 |
+
edge Cargo.lock scripts/analyze_rust_filter_add_command.py
|
| 3317 |
+
edge crates/urt-gameplay/Cargo.toml scripts/analyze_rust_filter_add_command.py
|
| 3318 |
+
edge crates/urt-gameplay/src/lib.rs scripts/analyze_rust_filter_add_command.py
|
| 3319 |
+
edge crates/urt-gameplay/src/filter_loading.rs scripts/analyze_rust_filter_add_command.py
|
| 3320 |
+
edge crates/urt-gameplay/src/filter_lifecycle.rs scripts/analyze_rust_filter_add_command.py
|
| 3321 |
+
edge crates/urt-gameplay/src/native_policy.rs scripts/analyze_rust_filter_add_command.py
|
| 3322 |
+
edge crates/urt-gameplay/src/bin/urt-filter-loading.rs scripts/analyze_rust_filter_add_command.py
|
| 3323 |
+
edge crates/urt-gameplay/src/qvm_format.rs scripts/analyze_rust_filter_add_command.py
|
| 3324 |
+
edge docs/research/qvm-filter-add-command.json scripts/analyze_rust_filter_add_command.py
|
| 3325 |
+
group rust-filter-add-command scripts/analyze_rust_health_impulse.py
|
| 3326 |
+
group gameplay scripts/analyze_rust_filter_add_command.py
|
| 3327 |
+
edge crates/urt-gameplay/src/filter_commands.rs scripts/analyze_rust_filter_add_command.py
|
| 3328 |
+
edge crates/urt-gameplay/src/bin/urt-filter-commands.rs scripts/analyze_rust_filter_add_command.py
|
| 3329 |
+
edge crates/urt-gameplay/src/filter_commands.rs scripts/analyze_rust_health_impulse.py
|
| 3330 |
+
edge crates/urt-gameplay/src/bin/urt-filter-commands.rs scripts/analyze_rust_health_impulse.py
|