nmarshall commited on
Commit
b7981cc
·
1 Parent(s): 289bfb3

gameplay: port filter expiration command effects

Browse files
crates/urt-gameplay/src/bin/urt-filter-loading.rs CHANGED
@@ -62,6 +62,7 @@ fn main() {
62
  );
63
  }
64
  "load" => println!("{}", loading(&w)),
 
65
  _ => panic!("unknown policy"),
66
  }
67
  }
@@ -171,3 +172,71 @@ fn loading(w: &[u32]) -> String {
171
  .unwrap();
172
  load_json(out, &rows, count)
173
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  );
63
  }
64
  "load" => println!("{}", loading(&w)),
65
+ "duration" => println!("{}", duration(&w)),
66
  _ => panic!("unknown policy"),
67
  }
68
  }
 
172
  .unwrap();
173
  load_json(out, &rows, count)
174
  }
175
+
176
+ fn duration(w: &[u32]) -> String {
177
+ let mut pos = 3;
178
+ let address = blob(w, &mut pos);
179
+ let duration = blob(w, &mut pos);
180
+ assert_eq!(pos, w.len());
181
+ let fields = [13, w[0], 0, 4, 5, 126, 6, 0, 0];
182
+ let client = if w[1] == u32::MAX {
183
+ None
184
+ } else {
185
+ Some(w[1] as i32)
186
+ };
187
+ let mut ring = w[2];
188
+ let mut rows = [
189
+ urt_gameplay::native_policy::FilterRecord {
190
+ mask: u32::MAX,
191
+ comparison: 0x04030201,
192
+ metadata: 77,
193
+ },
194
+ urt_gameplay::native_policy::FilterRecord {
195
+ mask: u32::MAX,
196
+ comparison: 0x04030201,
197
+ metadata: 88,
198
+ },
199
+ ];
200
+ let p = filter_duration(&mut rows, &address, &duration, &fields, client, &mut ring).unwrap();
201
+ let effects = p
202
+ .effects
203
+ .iter()
204
+ .map(|e| match e {
205
+ FilterDurationEffect::Console(t) => {
206
+ format!(r#"{{"op":"console","text":"{}"}}"#, hex(t))
207
+ }
208
+ FilterDurationEffect::Send { recipient, text } => format!(
209
+ r#"{{"op":"send","recipient":{},"text":"{}"}}"#,
210
+ recipient,
211
+ hex(text)
212
+ ),
213
+ FilterDurationEffect::Time => r#"{"op":"time"}"#.into(),
214
+ FilterDurationEffect::Metadata { text, value } => format!(
215
+ r#"{{"op":"metadata","text":"{}","metadata":{},"save":1}}"#,
216
+ hex(text),
217
+ value
218
+ ),
219
+ })
220
+ .collect::<Vec<String>>()
221
+ .join(",");
222
+ let updates = p
223
+ .update
224
+ .map(|p| {
225
+ format!(
226
+ r#"{{"result":{},"diagnostics":[{}],"exports":[{}]}}"#,
227
+ u8::from(p.result),
228
+ notice_json(&p),
229
+ if p.export { "252500" } else { "" }
230
+ )
231
+ })
232
+ .unwrap_or_default();
233
+ let rows = rows
234
+ .iter()
235
+ .map(|r| list(&[r.mask, r.comparison, r.metadata]))
236
+ .collect::<Vec<_>>()
237
+ .join(",");
238
+ format!(
239
+ r#"{{"events":[{}],"updates":[{}],"ring":{},"rows":[{}]}}"#,
240
+ effects, updates, ring, rows
241
+ )
242
+ }
crates/urt-gameplay/src/filter_loading.rs CHANGED
@@ -295,3 +295,116 @@ mod load_tests {
295
  assert_eq!(count, old);
296
  }
297
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
295
  assert_eq!(count, old);
296
  }
297
  }
298
+
299
+ #[derive(Clone, Debug, PartialEq, Eq)]
300
+ pub enum FilterDurationEffect {
301
+ Console(Vec<u8>),
302
+ Send { recipient: i32, text: Vec<u8> },
303
+ Time,
304
+ Metadata { text: Vec<u8>, value: u32 },
305
+ }
306
+ #[derive(Clone, Debug, PartialEq, Eq)]
307
+ pub struct FilterDuration {
308
+ pub effects: Vec<FilterDurationEffect>,
309
+ pub update: Option<FilterMutation>,
310
+ }
311
+ /// Original252213: empty arguments select usage; otherwise add parsed duration
312
+ /// to supplied current minutes and request first-match metadata update with save.
313
+ pub fn filter_duration(
314
+ records: &mut [FilterRecord],
315
+ address: &[u8],
316
+ duration: &[u8],
317
+ fields: &[u32; 9],
318
+ client: Option<i32>,
319
+ ring: &mut u32,
320
+ ) -> Result<FilterDuration, Error> {
321
+ if records.len() > 4096 {
322
+ return Err(Error::InvalidCount);
323
+ }
324
+ if client.is_some_and(|s| !(0..64).contains(&s)) {
325
+ return Err(Error::InvalidIndex);
326
+ }
327
+ let address = address.split(|b| *b == 0).next().unwrap_or_default();
328
+ let duration = duration.split(|b| *b == 0).next().unwrap_or_default();
329
+ if address.len() > 1024 || duration.len() > 1024 {
330
+ return Err(Error::InvalidInput);
331
+ }
332
+ if address.is_empty() || duration.is_empty() {
333
+ let effect = if let Some(recipient) = client {
334
+ let mut text = crate::qvm_format::render(
335
+ b"chat \"^5[rcon] ^7Usage: /addipexpire <ip-mask> <minutes>\"",
336
+ &[],
337
+ )
338
+ .map_err(|_| Error::InvalidInput)?;
339
+ text.pop();
340
+ *ring = ring.wrapping_add(1);
341
+ FilterDurationEffect::Send { recipient, text }
342
+ } else {
343
+ FilterDurationEffect::Console(b"Usage: addipexpire <ip-mask> <minutes>\n".to_vec())
344
+ };
345
+ return Ok(FilterDuration {
346
+ effects: vec![effect],
347
+ update: None,
348
+ });
349
+ }
350
+ let value =
351
+ filter_minutes(fields).wrapping_add(crate::native_policy::signed_prefix(duration) as u32);
352
+ let mut staged = records.to_vec();
353
+ let update = update_filter_metadata(&mut staged, address, value, 1)?;
354
+ records.copy_from_slice(&staged);
355
+ Ok(FilterDuration {
356
+ effects: vec![
357
+ FilterDurationEffect::Time,
358
+ FilterDurationEffect::Metadata {
359
+ text: address.to_vec(),
360
+ value,
361
+ },
362
+ ],
363
+ update: Some(update),
364
+ })
365
+ }
366
+ #[cfg(test)]
367
+ mod duration_tests {
368
+ use super::*;
369
+ #[test]
370
+ fn usage_wraps_only_client_ring_and_skips_time() {
371
+ let mut ring = u32::MAX;
372
+ let p = filter_duration(&mut [], b"", b"5", &[0; 9], Some(3), &mut ring).unwrap();
373
+ assert_eq!(ring, 0);
374
+ assert!(p.update.is_none());
375
+ assert!(matches!(
376
+ &p.effects[..],
377
+ [FilterDurationEffect::Send { recipient: 3, .. }]
378
+ ));
379
+ filter_duration(&mut [], b"1.2.3.4", b"", &[0; 9], None, &mut ring).unwrap();
380
+ assert_eq!(ring, 0);
381
+ }
382
+ #[test]
383
+ fn negative_duration_can_store_permanent_marker_and_preserves_duplicate() {
384
+ let row = FilterRecord {
385
+ mask: u32::MAX,
386
+ comparison: 0x04030201,
387
+ metadata: 77,
388
+ };
389
+ let mut rows = [row, row];
390
+ let mut ring = 7;
391
+ let p = filter_duration(&mut rows, b"1.2.3.4", b"-1", &[0; 9], None, &mut ring).unwrap();
392
+ assert!(p.update.unwrap().export);
393
+ assert_eq!(rows[0].metadata, u32::MAX);
394
+ assert_eq!(rows[1], row);
395
+ assert_eq!(ring, 7);
396
+ }
397
+ #[test]
398
+ fn unsupported_address_rejects_without_row_or_ring_changes() {
399
+ let row = FilterRecord {
400
+ mask: 7,
401
+ comparison: 8,
402
+ metadata: 9,
403
+ };
404
+ let mut rows = [row];
405
+ let mut ring = 7;
406
+ assert!(filter_duration(&mut rows, &[b'1'; 128], b"5", &[0; 9], None, &mut ring).is_err());
407
+ assert_eq!(rows, [row]);
408
+ assert_eq!(ring, 7);
409
+ }
410
+ }
docs/FINDINGS.md CHANGED
@@ -1833,3 +1833,5 @@ RE-948: Thirty-two original252933 fixtures compose actual insertion/metadata/par
1833
  RE-949: Rust matches32 original loader compositions, preserving active count, first-prior-match metadata routing, short-read buffer provenance, NUL/line handling and unconditional read/close effects. The staged API accepts explicit declared length, delivered bytes, initial buffer and raw handle. It rejects unsafe negative lengths, overlong lines/tokens and insufficient table storage atomically. Unit tests cover duplicates, missing-file replies, stale partial reads and rollback after an unsupported later line. [Evidence](research/RUST_FILTER_LOAD.md).
1834
 
1835
  RE-950: Eighty actual252213 fixtures compose integer/time/metadata children, preserving wrapping duration-plus-current-minute arithmetic, exact first-match metadata writes and save requests. Empty arguments emit exact console or client usage without clock/update calls; client formatting advances and wraps the ring. Negative/overflowing duration prefixes are retained. Native clock/calendar semantics, sends and export execution remain explicit boundaries. [Evidence](research/QVM_FILTER_DURATION.md).
 
 
 
1833
  RE-949: Rust matches32 original loader compositions, preserving active count, first-prior-match metadata routing, short-read buffer provenance, NUL/line handling and unconditional read/close effects. The staged API accepts explicit declared length, delivered bytes, initial buffer and raw handle. It rejects unsafe negative lengths, overlong lines/tokens and insufficient table storage atomically. Unit tests cover duplicates, missing-file replies, stale partial reads and rollback after an unsupported later line. [Evidence](research/RUST_FILTER_LOAD.md).
1834
 
1835
  RE-950: Eighty actual252213 fixtures compose integer/time/metadata children, preserving wrapping duration-plus-current-minute arithmetic, exact first-match metadata writes and save requests. Empty arguments emit exact console or client usage without clock/update calls; client formatting advances and wraps the ring. Negative/overflowing duration prefixes are retained. Native clock/calendar semantics, sends and export execution remain explicit boundaries. [Evidence](research/QVM_FILTER_DURATION.md).
1836
+
1837
+ RE-951: Rust matches80 original duration/usage command fixtures, preserving wrapping numeric prefixes, first-match metadata save requests, exact client/console text and formatter-counter wrap. Typed effects preserve time-before-update order; usage skips both. Unit tests cover client-only ring changes, negative duration producing permanent-1 metadata, duplicate preservation and atomic unsupported-address rejection. Native replies remain supplied or captured. [Evidence](research/RUST_FILTER_DURATION.md).
docs/PROVISIONAL_TODO.md CHANGED
@@ -1881,3 +1881,5 @@ RE-934–943 completed bounded filter export/removal/metadata and insertion/remo
1881
  - [x] RE-949 Rust bounded loading with explicit read replies ([evidence](research/RUST_FILTER_LOAD.md)).
1882
 
1883
  - [x] RE-950 Original expiration-duration command and usage paths ([evidence](research/QVM_FILTER_DURATION.md)).
 
 
 
1881
  - [x] RE-949 Rust bounded loading with explicit read replies ([evidence](research/RUST_FILTER_LOAD.md)).
1882
 
1883
  - [x] RE-950 Original expiration-duration command and usage paths ([evidence](research/QVM_FILTER_DURATION.md)).
1884
+
1885
+ - [x] RE-951 Rust expiration command with typed time and usage effects ([evidence](research/RUST_FILTER_DURATION.md)).
docs/RESEARCH_LEDGER.md CHANGED
@@ -2467,3 +2467,7 @@ Rust matches32 original loader compositions, preserving active count, first-prio
2467
  ## 2026-10-28 — RE-950 Original expiration-duration command and usage paths
2468
 
2469
  Eighty actual252213 fixtures compose integer/time/metadata children, preserving wrapping duration-plus-current-minute arithmetic, exact first-match metadata writes and save requests. Empty arguments emit exact console or client usage without clock/update calls; client formatting advances and wraps the ring. Negative/overflowing duration prefixes are retained. Native clock/calendar semantics, sends and export execution remain explicit boundaries. Focused `cargo xtask test-group qvm-filter-duration` 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.
 
 
 
 
 
2467
  ## 2026-10-28 — RE-950 Original expiration-duration command and usage paths
2468
 
2469
  Eighty actual252213 fixtures compose integer/time/metadata children, preserving wrapping duration-plus-current-minute arithmetic, exact first-match metadata writes and save requests. Empty arguments emit exact console or client usage without clock/update calls; client formatting advances and wraps the ring. Negative/overflowing duration prefixes are retained. Native clock/calendar semantics, sends and export execution remain explicit boundaries. Focused `cargo xtask test-group qvm-filter-duration` 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.
2470
+
2471
+ ## 2026-10-28 — RE-951 Rust expiration command with typed time and usage effects
2472
+
2473
+ Rust matches80 original duration/usage command fixtures, preserving wrapping numeric prefixes, first-match metadata save requests, exact client/console text and formatter-counter wrap. Typed effects preserve time-before-update order; usage skips both. Unit tests cover client-only ring changes, negative duration producing permanent-1 metadata, duplicate preservation and atomic unsupported-address rejection. Native replies remain supplied or captured. Focused `cargo xtask test-group rust-filter-duration` 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
@@ -1918,3 +1918,5 @@ RE-934–943 continue the prior batch with filter export/removal/metadata and co
1918
  - [x] RE-949 Rust bounded loading with explicit read replies ([evidence](research/RUST_FILTER_LOAD.md)).
1919
 
1920
  - [x] RE-950 Original expiration-duration command and usage paths ([evidence](research/QVM_FILTER_DURATION.md)).
 
 
 
1918
  - [x] RE-949 Rust bounded loading with explicit read replies ([evidence](research/RUST_FILTER_LOAD.md)).
1919
 
1920
  - [x] RE-950 Original expiration-duration command and usage paths ([evidence](research/QVM_FILTER_DURATION.md)).
1921
+
1922
+ - [x] RE-951 Rust expiration command with typed time and usage effects ([evidence](research/RUST_FILTER_DURATION.md)).
docs/research/README.md CHANGED
@@ -1453,3 +1453,5 @@ The extension collector adds five focused tests for native tail calls, command-r
1453
  - [RE-949: Rust bounded loading with explicit read replies](RUST_FILTER_LOAD.md).
1454
 
1455
  - [RE-950: Original expiration-duration command and usage paths](QVM_FILTER_DURATION.md).
 
 
 
1453
  - [RE-949: Rust bounded loading with explicit read replies](RUST_FILTER_LOAD.md).
1454
 
1455
  - [RE-950: Original expiration-duration command and usage paths](QVM_FILTER_DURATION.md).
1456
+
1457
+ - [RE-951: Rust expiration command with typed time and usage effects](RUST_FILTER_DURATION.md).
docs/research/RUST_FILTER_DURATION.md ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ # Rust expiration command with typed time and usage effects
2
+
3
+ RE-951. Focused validation: `cargo xtask test-group rust-filter-duration`. [Machine report](rust-filter-duration.json).
4
+
5
+ Rust matches80 original duration/usage command fixtures, preserving wrapping numeric prefixes, first-match metadata save requests, exact client/console text and formatter-counter wrap. Typed effects preserve time-before-update order; usage skips both. Unit tests cover client-only ring changes, negative duration producing permanent-1 metadata, duplicate preservation and atomic unsupported-address rejection. Native replies remain supplied or captured.
docs/research/rust-filter-duration.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": 203,
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": 206,
2989
  "source_report": "docs/research/qvm-health-impulse.json",
2990
  "source_report_sha256": "625fd8064004ea6dbdf398d51d0af844d9c635ff04c0a0790886dddb6ab81e71"
2991
  }
scripts/analyze_rust_filter_duration.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-loading'],check=True,capture_output=True)
9
+ binary=Path(os.environ.get('CARGO_TARGET_DIR','target'))/'debug'/'urt-filter-loading'
10
+ path=Path('docs/research/qvm-filter-duration.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', 'updates', 'ring', 'rows']},rust=result(binary,'duration',encode(p['input']))))
13
+ write_json(Path('docs/research/rust-filter-duration.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
+ ip=bytes.fromhex(a['address']);d=bytes.fromhex(a['duration']);return [a['now'],a['client'],a['ring'],len(ip),*ip,len(d),*d]
17
+ if __name__=='__main__':main()
scripts/test_rust_filter_duration.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-duration.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-duration.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
@@ -3268,3 +3268,19 @@ report scripts/analyze_qvm_filter_duration.py docs/research/qvm-filter-duration.
3268
  edge docs/research/qvm-filter-duration.json scripts/test_qvm_filter_duration.py
3269
  group qvm-filter-duration scripts/analyze_qvm_filter_duration.py
3270
  edge scripts/analyze_qvm_filter_duration.py scripts/analyze_qvm_resumer_audit.py
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3268
  edge docs/research/qvm-filter-duration.json scripts/test_qvm_filter_duration.py
3269
  group qvm-filter-duration scripts/analyze_qvm_filter_duration.py
3270
  edge scripts/analyze_qvm_filter_duration.py scripts/analyze_qvm_resumer_audit.py
3271
+ report scripts/analyze_rust_filter_duration.py docs/research/rust-filter-duration.json
3272
+ edge docs/research/rust-filter-duration.json scripts/test_rust_filter_duration.py
3273
+ group rust-filter-duration scripts/analyze_rust_filter_duration.py
3274
+ edge scripts/analyze_rust_filter_duration.py scripts/analyze_qvm_resumer_audit.py
3275
+ edge Cargo.toml scripts/analyze_rust_filter_duration.py
3276
+ edge Cargo.lock scripts/analyze_rust_filter_duration.py
3277
+ edge crates/urt-gameplay/Cargo.toml scripts/analyze_rust_filter_duration.py
3278
+ edge crates/urt-gameplay/src/lib.rs scripts/analyze_rust_filter_duration.py
3279
+ edge crates/urt-gameplay/src/filter_loading.rs scripts/analyze_rust_filter_duration.py
3280
+ edge crates/urt-gameplay/src/filter_lifecycle.rs scripts/analyze_rust_filter_duration.py
3281
+ edge crates/urt-gameplay/src/native_policy.rs scripts/analyze_rust_filter_duration.py
3282
+ edge crates/urt-gameplay/src/bin/urt-filter-loading.rs scripts/analyze_rust_filter_duration.py
3283
+ edge crates/urt-gameplay/src/qvm_format.rs scripts/analyze_rust_filter_duration.py
3284
+ edge docs/research/qvm-filter-duration.json scripts/analyze_rust_filter_duration.py
3285
+ group rust-filter-duration scripts/analyze_rust_health_impulse.py
3286
+ group gameplay scripts/analyze_rust_filter_duration.py