nmarshall commited on
Commit
01f224d
·
1 Parent(s): ec64722

gameplay: port add-filter command and queued kick

Browse files
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": 209,
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