Skip to main content

smb_server_testsuite/
cases.rs

1//! The conformance case registry.
2//!
3//! Categories mirror Microsoft's WindowsProtocolTestSuites FileServer family
4//! (MS-SMB2 / MS-FSCC). Each case drives the server through the Python
5//! `smbprotocol` actuator and asserts on the returned JSON. Cases must leave
6//! no residue on the share (created files use delete-on-close).
7
8use serde_json::{json, Value};
9
10use crate::{Ctx, Metrics, Opts, TestCase};
11
12/// The full catalog, in a stable order.
13#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] // SMB2 test-fixture sizes/offsets
14pub fn all() -> Vec<TestCase> {
15    vec![
16        // ---- Negotiate ([MS-SMB2] §2.2.3/4) ----
17        TestCase {
18            id: "negotiate.default",
19            category: "Negotiate",
20            spec: "MS-SMB2 3.1.1 §2.2.3",
21            about: "Connects with the highest common dialect and reaches the share",
22            run: |c| { probe(c, &Opts::default())?; Ok(Metrics::new()) },
23        },
24        TestCase {
25            id: "negotiate.smb_3_1_1",
26            category: "Negotiate",
27            spec: "MS-SMB2 §2.2.3.1.1",
28            about: "Pins dialect 3.1.1 and verifies it is negotiated",
29            run: |c| { expect_dialect(c, "3.1.1")?; Ok(Metrics::new()) },
30        },
31        TestCase {
32            id: "negotiate.smb_2_1_0",
33            category: "Negotiate",
34            spec: "MS-SMB2 §2.2.3",
35            about: "Pins dialect 2.1.0 and verifies it is negotiated",
36            run: |c| { expect_dialect(c, "2.1.0")?; Ok(Metrics::new()) },
37        },
38        TestCase {
39            id: "negotiate.smb_2_0_2",
40            category: "Negotiate",
41            spec: "MS-SMB2 §2.2.3",
42            about: "Pins dialect 2.0.2 and verifies it is negotiated",
43            run: |c| { expect_dialect(c, "2.0.2")?; Ok(Metrics::new()) },
44        },
45        // ---- Session ([MS-SMB2] §2.2.5, [MS-NLMP]) ----
46        TestCase {
47            id: "session.ntlm_auth",
48            category: "Session",
49            spec: "MS-SMB2 §2.2.5 / MS-NLMP",
50            about: "SPNEGO/NTLMSSP session setup reaches the share",
51            run: |c| { probe(c, &Opts::default())?; Ok(Metrics::new()) },
52        },
53        TestCase {
54            id: "session.signed",
55            category: "Session",
56            spec: "MS-SMB2 §3.2.5.3",
57            about: "Signed session (require_signing) completes an op",
58            run: |c| { probe(c, &Opts::signed())?; Ok(Metrics::new()) },
59        },
60        // ---- Encryption ([MS-SMB2] §3.3.5.16) ----
61        TestCase {
62            id: "encryption.aes_roundtrip",
63            category: "Encryption",
64            spec: "MS-SMB2 §2.2.41",
65            about: "Encrypted session write+read roundtrip preserves data",
66            run: |c| { roundtrip(c, &Opts::encrypted(), "enc_probe.bin", 4096)?; Ok(Metrics::new()) },
67        },
68        // ---- Tree connect ([MS-SMB2] §2.2.9) ----
69        TestCase {
70            id: "tree.connect_share",
71            category: "TreeConnect",
72            spec: "MS-SMB2 §2.2.9/10",
73            about: "TREE_CONNECT to the configured share succeeds",
74            run: |c| { probe(c, &Opts::default())?; Ok(Metrics::new()) },
75        },
76        // ---- Create ([MS-SMB2] §2.2.13) ----
77        TestCase {
78            id: "create.create_new",
79            category: "Create",
80            spec: "MS-SMB2 §2.2.13",
81            about: "FILE_CREATE makes a new file (delete-on-close cleans up)",
82            run: |c| {
83                let r = c.run(json!([
84                    {"op":"open","handle":"f","path":"ct_create_new.bin",
85                     "disposition":"create","delete_on_close":true},
86                    {"op":"close","handle":"f"}
87                ]))?;
88                ok(&r)
89            },
90        },
91        TestCase {
92            id: "create.overwrite_if",
93            category: "Create",
94            spec: "MS-SMB2 §2.2.13",
95            about: "FILE_OVERWRITE_IF opens-or-truncates and writes",
96            run: |c| { roundtrip(c, &Opts::default(), "ct_overwrite_if.bin", 1024)?; Ok(Metrics::new()) },
97        },
98        TestCase {
99            id: "create.open_missing_fails",
100            category: "Create",
101            spec: "MS-SMB2 §3.3.5.9",
102            about: "FILE_OPEN of a missing file is refused",
103            run: |c| {
104                let r = c.run(json!([
105                    {"op":"open","handle":"f","path":"ct_definitely_absent_9271.bin",
106                     "disposition":"open"}
107                ]))?;
108                if r.ok {
109                    return Err("opening a missing file unexpectedly succeeded".into());
110                }
111                Ok(Metrics::new())
112            },
113        },
114        TestCase {
115            id: "create.directory_open",
116            category: "Create",
117            spec: "MS-SMB2 §2.2.13",
118            about: "Opens the share root as a directory handle",
119            run: |c| {
120                let r = c.run(json!([
121                    {"op":"open","handle":"d","path":"","directory":true,"disposition":"open"},
122                    {"op":"close","handle":"d"}
123                ]))?;
124                ok(&r)
125            },
126        },
127        // ---- Read / Write ([MS-SMB2] §2.2.19/21) ----
128        TestCase {
129            id: "readwrite.roundtrip_small",
130            category: "ReadWrite",
131            spec: "MS-SMB2 §2.2.19/21",
132            about: "Small write then read returns identical bytes",
133            run: |c| { roundtrip(c, &Opts::default(), "rw_small.bin", 512)?; Ok(Metrics::new()) },
134        },
135        TestCase {
136            id: "readwrite.offset",
137            category: "ReadWrite",
138            spec: "MS-SMB2 §2.2.19",
139            about: "Write at a non-zero offset reads back at that offset",
140            run: |c| {
141                let r = c.run(json!([
142                    {"op":"open","handle":"f","path":"rw_offset.bin",
143                     "disposition":"overwrite_if","delete_on_close":true},
144                    {"op":"write","handle":"f","offset":4096,"fill_size":256,"fill_seed":11},
145                    {"op":"read","handle":"f","offset":4096,"length":256},
146                    {"op":"close","handle":"f"}
147                ]))?;
148                ok(&r)?;
149                compare_crc(&r, 1, 2)
150            },
151        },
152        TestCase {
153            id: "readwrite.large_mtu_256k",
154            category: "ReadWrite",
155            spec: "MS-SMB2 §2.2.3.1.1 CAP_LARGE_MTU",
156            about: "256 KiB single-op write+read (multi-credit) preserves data",
157            run: |c| {
158                let size = 256 * 1024u64;
159                let r = c.run(json!([
160                    {"op":"open","handle":"f","path":"rw_large.bin",
161                     "disposition":"overwrite_if","delete_on_close":true},
162                    {"op":"write","handle":"f","offset":0,"fill_size":size,"fill_seed":7},
163                    {"op":"read","handle":"f","offset":0,"length":size},
164                    {"op":"close","handle":"f"}
165                ]))?;
166                ok(&r)?;
167                compare_crc(&r, 1, 2)?;
168                let mut m = Metrics::new();
169                // End-to-end throughput incl. client startup; a trend signal.
170                m.insert("bytes".into(), (size * 2) as f64);
171                Ok(m)
172            },
173        },
174        // ---- Directory query ([MS-SMB2] §2.2.33) ----
175        TestCase {
176            id: "directory.enumerate",
177            category: "Directory",
178            spec: "MS-SMB2 §2.2.33/34",
179            about: "Created files appear in a directory enumeration",
180            run: |c| {
181                let r = c.run(json!([
182                    {"op":"open","handle":"a","path":"dir_enum_a.bin","disposition":"overwrite_if"},
183                    {"op":"close","handle":"a"},
184                    {"op":"open","handle":"b","path":"dir_enum_b.bin","disposition":"overwrite_if"},
185                    {"op":"close","handle":"b"},
186                    {"op":"list","path":"","pattern":"dir_enum_*"},
187                    {"op":"open","handle":"a","path":"dir_enum_a.bin","disposition":"open","delete_on_close":true},
188                    {"op":"close","handle":"a"},
189                    {"op":"open","handle":"b","path":"dir_enum_b.bin","disposition":"open","delete_on_close":true},
190                    {"op":"close","handle":"b"}
191                ]))?;
192                ok(&r)?;
193                let names = list_names(&r, 4)?;
194                for want in ["dir_enum_a.bin", "dir_enum_b.bin"] {
195                    if !names.iter().any(|n| n == want) {
196                        return Err(format!("enumeration missing {want}: got {names:?}"));
197                    }
198                }
199                Ok(Metrics::new())
200            },
201        },
202        TestCase {
203            id: "directory.pattern_filter",
204            category: "Directory",
205            spec: "MS-SMB2 §2.2.33.1 / MS-CIFS wildcard",
206            about: "A wildcard pattern only returns matching names",
207            run: |c| {
208                let r = c.run(json!([
209                    {"op":"open","handle":"m","path":"pat_match.bin","disposition":"overwrite_if"},
210                    {"op":"close","handle":"m"},
211                    {"op":"open","handle":"o","path":"pat_other.bin","disposition":"overwrite_if"},
212                    {"op":"close","handle":"o"},
213                    {"op":"list","path":"","pattern":"pat_match*"},
214                    {"op":"open","handle":"m","path":"pat_match.bin","disposition":"open","delete_on_close":true},
215                    {"op":"close","handle":"m"},
216                    {"op":"open","handle":"o","path":"pat_other.bin","disposition":"open","delete_on_close":true},
217                    {"op":"close","handle":"o"}
218                ]))?;
219                ok(&r)?;
220                let names = list_names(&r, 4)?;
221                if names.iter().any(|n| n == "pat_other.bin") {
222                    return Err(format!("pattern leaked a non-match: {names:?}"));
223                }
224                if !names.iter().any(|n| n == "pat_match.bin") {
225                    return Err(format!("pattern dropped the match: {names:?}"));
226                }
227                Ok(Metrics::new())
228            },
229        },
230        // ---- IOCTL / FSCTL ([MS-SMB2] §2.2.31) ----
231        TestCase {
232            id: "ioctl.validate_negotiate_info",
233            category: "Ioctl",
234            spec: "MS-SMB2 §3.3.5.15.12",
235            about: "FSCTL_VALIDATE_NEGOTIATE_INFO on 3.1.1 terminates the connection",
236            run: |c| {
237                // On 3.1.1 pre-auth integrity supersedes this exchange, so the
238                // server MUST terminate the connection ([MS-SMB2] §3.3.5.15.12).
239                let r = c.run_with(&Opts::dialect("3.1.1"), json!([
240                    {"op":"ioctl","ctl_code":0x00140204,
241                     "input_b64":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"}
242                ]))?;
243                if r.ok {
244                    return Err("expected connection termination on 3.1.1 validate-negotiate".into());
245                }
246                let err = r.error.clone().unwrap_or_default();
247                if err.to_lowercase().contains("clos") {
248                    Ok(Metrics::new())
249                } else {
250                    Err(format!("expected connection closed, got: {err}"))
251                }
252            },
253        },
254        TestCase {
255            id: "ioctl.query_network_interface_info",
256            category: "Ioctl",
257            spec: "MS-SMB2 §3.3.5.15.4",
258            about: "FSCTL_QUERY_NETWORK_INTERFACE_INFO returns interface data",
259            run: |c| {
260                let r = c.run_with(&Opts::dialect("3.1.1"), json!([
261                    {"op":"ioctl","ctl_code":0x001401FC}
262                ]))?;
263                ok(&r)?;
264                let out = b64_len(&r, 0)?;
265                if out < 152 {
266                    return Err(format!("interface-info output too short: {out}"));
267                }
268                Ok(Metrics::new())
269            },
270        },
271        // ---- Query info ([MS-SMB2] §2.2.37, [MS-FSCC] §2.4) ----
272        TestCase {
273            id: "queryinfo.standard",
274            category: "QueryInfo",
275            spec: "MS-FSCC §2.4.41 FileStandardInformation",
276            about: "FileStandardInformation reports the written EOF",
277            run: |c| {
278                let r = c.run(json!([
279                    {"op":"open","handle":"f","path":"qi_std.bin","disposition":"overwrite_if","delete_on_close":true},
280                    {"op":"write","handle":"f","offset":0,"fill_size":1000},
281                    {"op":"query_info","handle":"f","info_type":"file","info_class":5},
282                    {"op":"close","handle":"f"}
283                ]))?;
284                ok(&r)?;
285                let eof = step_u64(&r, 2, "end_of_file")?;
286                if eof != 1000 {
287                    return Err(format!("end_of_file {eof} != 1000"));
288                }
289                Ok(Metrics::new())
290            },
291        },
292        TestCase {
293            id: "queryinfo.all_information",
294            category: "QueryInfo",
295            spec: "MS-FSCC §2.4.2 FileAllInformation",
296            about: "FileAllInformation returns a populated buffer",
297            run: |c| {
298                let r = c.run(json!([
299                    {"op":"open","handle":"f","path":"qi_all.bin","disposition":"overwrite_if","delete_on_close":true},
300                    {"op":"query_info","handle":"f","info_type":"file","info_class":18},
301                    {"op":"close","handle":"f"}
302                ]))?;
303                ok(&r)?;
304                if step_u64(&r, 1, "length")? == 0 {
305                    return Err("FileAllInformation returned empty buffer".into());
306                }
307                Ok(Metrics::new())
308            },
309        },
310        TestCase {
311            id: "queryinfo.network_open",
312            category: "QueryInfo",
313            spec: "MS-FSCC §2.4.34 FileNetworkOpenInformation",
314            about: "FileNetworkOpenInformation returns a populated buffer",
315            run: |c| {
316                let r = c.run(json!([
317                    {"op":"open","handle":"f","path":"qi_netopen.bin","disposition":"overwrite_if","delete_on_close":true},
318                    {"op":"query_info","handle":"f","info_type":"file","info_class":34},
319                    {"op":"close","handle":"f"}
320                ]))?;
321                ok(&r)?;
322                if step_u64(&r, 1, "length")? == 0 {
323                    return Err("FileNetworkOpenInformation returned empty buffer".into());
324                }
325                Ok(Metrics::new())
326            },
327        },
328        // ---- Set info ([MS-SMB2] §2.2.39, [MS-FSCC] §2.4) ----
329        TestCase {
330            id: "setinfo.end_of_file",
331            category: "SetInfo",
332            spec: "MS-FSCC §2.4.13 FileEndOfFileInformation",
333            about: "Setting EOF resizes the file (verified via query)",
334            run: |c| {
335                let r = c.run(json!([
336                    {"op":"open","handle":"f","path":"si_eof.bin","disposition":"overwrite_if","delete_on_close":true},
337                    {"op":"set_info","handle":"f","kind":"eof","size":4096},
338                    {"op":"query_info","handle":"f","info_type":"file","info_class":5},
339                    {"op":"close","handle":"f"}
340                ]))?;
341                ok(&r)?;
342                let eof = step_u64(&r, 2, "end_of_file")?;
343                if eof != 4096 {
344                    return Err(format!("end_of_file {eof} != 4096 after set-eof"));
345                }
346                Ok(Metrics::new())
347            },
348        },
349        TestCase {
350            id: "setinfo.rename",
351            category: "SetInfo",
352            spec: "MS-FSCC §2.4.37 FileRenameInformation",
353            about: "Rename moves the file to the new name",
354            run: |c| {
355                let r = c.run(json!([
356                    {"op":"open","handle":"f","path":"si_rename_a.bin","disposition":"overwrite_if"},
357                    {"op":"set_info","handle":"f","kind":"rename","new_name":"si_rename_b.bin","replace":true},
358                    {"op":"close","handle":"f"},
359                    {"op":"list","path":"","pattern":"si_rename_*"},
360                    {"op":"open","handle":"b","path":"si_rename_b.bin","disposition":"open","delete_on_close":true},
361                    {"op":"close","handle":"b"}
362                ]))?;
363                ok(&r)?;
364                let names = list_names(&r, 3)?;
365                if names.iter().any(|n| n == "si_rename_a.bin") {
366                    return Err("old name still present after rename".into());
367                }
368                if !names.iter().any(|n| n == "si_rename_b.bin") {
369                    return Err(format!("new name missing after rename: {names:?}"));
370                }
371                Ok(Metrics::new())
372            },
373        },
374        TestCase {
375            id: "setinfo.delete_disposition",
376            category: "SetInfo",
377            spec: "MS-FSCC §2.4.11 FileDispositionInformation",
378            about: "Delete-on-close disposition removes the file",
379            run: |c| {
380                // The final open is expected to fail: the file is gone.
381                let r = c.run(json!([
382                    {"op":"open","handle":"f","path":"si_del.bin","disposition":"overwrite_if"},
383                    {"op":"set_info","handle":"f","kind":"delete"},
384                    {"op":"close","handle":"f"},
385                    {"op":"open","handle":"g","path":"si_del.bin","disposition":"open","expect_fail":true}
386                ]))?;
387                ok(&r)
388            },
389        },
390        // ---- Locking ([MS-SMB2] §2.2.26) ----
391        TestCase {
392            id: "locking.exclusive_conflict",
393            category: "Locking",
394            spec: "MS-SMB2 §2.2.26 / §3.3.5.14",
395            about: "An exclusive lock blocks a conflicting lock until released",
396            run: |c| {
397                let r = c.run(json!([
398                    {"op":"open","handle":"a","path":"lk_conf.bin","disposition":"overwrite_if"},
399                    {"op":"write","handle":"a","offset":0,"fill_size":100},
400                    {"op":"open","handle":"b","path":"lk_conf.bin","disposition":"open"},
401                    {"op":"lock","handle":"a","kind":"exclusive","offset":0,"length":50},
402                    {"op":"lock","handle":"b","kind":"exclusive","offset":0,"length":50,"expect_fail":true},
403                    {"op":"lock","handle":"a","kind":"unlock","offset":0,"length":50},
404                    {"op":"lock","handle":"b","kind":"exclusive","offset":0,"length":50},
405                    {"op":"lock","handle":"b","kind":"unlock","offset":0,"length":50},
406                    {"op":"close","handle":"b"},
407                    {"op":"close","handle":"a","delete":true}
408                ]))?;
409                ok(&r)
410            },
411        },
412        TestCase {
413            id: "locking.unlock_roundtrip",
414            category: "Locking",
415            spec: "MS-SMB2 §2.2.26",
416            about: "Lock then unlock a range on one handle",
417            run: |c| {
418                let r = c.run(json!([
419                    {"op":"open","handle":"f","path":"lk_rt.bin","disposition":"overwrite_if","delete_on_close":true},
420                    {"op":"write","handle":"f","offset":0,"fill_size":64},
421                    {"op":"lock","handle":"f","kind":"exclusive","offset":0,"length":32},
422                    {"op":"lock","handle":"f","kind":"unlock","offset":0,"length":32},
423                    {"op":"close","handle":"f"}
424                ]))?;
425                ok(&r)
426            },
427        },
428        TestCase {
429            id: "locking.shared_compatible",
430            category: "Locking",
431            spec: "MS-SMB2 §2.2.26 / MS-FSA §2.1.5.9",
432            about: "Two shared locks on one range coexist; an exclusive lock over it is refused until both release",
433            run: |c| {
434                let r = c.run(json!([
435                    {"op":"open","handle":"a","path":"lk_shared.bin","disposition":"overwrite_if","delete_on_close":true},
436                    {"op":"write","handle":"a","offset":0,"fill_size":100},
437                    {"op":"open","handle":"b","path":"lk_shared.bin","disposition":"open"},
438                    {"op":"lock","handle":"a","kind":"shared","offset":0,"length":50},
439                    {"op":"lock","handle":"b","kind":"shared","offset":0,"length":50},
440                    {"op":"lock","handle":"b","kind":"exclusive","offset":0,"length":50,"expect_fail":true},
441                    {"op":"lock","handle":"a","kind":"unlock","offset":0,"length":50},
442                    {"op":"lock","handle":"b","kind":"unlock","offset":0,"length":50},
443                    {"op":"lock","handle":"b","kind":"exclusive","offset":0,"length":50},
444                    {"op":"lock","handle":"b","kind":"unlock","offset":0,"length":50},
445                    {"op":"close","handle":"b"},
446                    {"op":"close","handle":"a"}
447                ]))?;
448                ok(&r)
449            },
450        },
451        TestCase {
452            id: "locking.nonoverlap_ok",
453            category: "Locking",
454            spec: "MS-SMB2 §2.2.26 / MS-FSA §2.1.5.9",
455            about: "Exclusive locks on disjoint ranges of the same file do not conflict",
456            run: |c| {
457                let r = c.run(json!([
458                    {"op":"open","handle":"a","path":"lk_disjoint.bin","disposition":"overwrite_if","delete_on_close":true},
459                    {"op":"write","handle":"a","offset":0,"fill_size":200},
460                    {"op":"open","handle":"b","path":"lk_disjoint.bin","disposition":"open"},
461                    {"op":"lock","handle":"a","kind":"exclusive","offset":0,"length":50},
462                    {"op":"lock","handle":"b","kind":"exclusive","offset":100,"length":50},
463                    {"op":"lock","handle":"a","kind":"unlock","offset":0,"length":50},
464                    {"op":"lock","handle":"b","kind":"unlock","offset":100,"length":50},
465                    {"op":"close","handle":"b"},
466                    {"op":"close","handle":"a"}
467                ]))?;
468                ok(&r)
469            },
470        },
471        TestCase {
472            id: "locking.shared_blocks_write",
473            category: "Locking",
474            spec: "MS-FSA §2.1.5.9 (read lock forbids overlapping writes)",
475            about: "A shared (read) lock blocks a conflicting write by another open until released",
476            run: |c| {
477                let r = c.run(json!([
478                    {"op":"open","handle":"a","path":"lk_wblock.bin","disposition":"overwrite_if","delete_on_close":true},
479                    {"op":"write","handle":"a","offset":0,"fill_size":100},
480                    {"op":"open","handle":"b","path":"lk_wblock.bin","disposition":"open"},
481                    {"op":"lock","handle":"a","kind":"shared","offset":0,"length":50},
482                    {"op":"write","handle":"b","offset":0,"fill_size":50,"expect_fail":true},
483                    {"op":"lock","handle":"a","kind":"unlock","offset":0,"length":50},
484                    {"op":"write","handle":"b","offset":0,"fill_size":50},
485                    {"op":"close","handle":"b"},
486                    {"op":"close","handle":"a"}
487                ]))?;
488                ok(&r)
489            },
490        },
491        TestCase {
492            id: "locking.exclusive_blocks_read",
493            category: "Locking",
494            spec: "MS-FSA §2.1.5.9 (exclusive lock forbids other reads)",
495            about: "An exclusive lock blocks a read by another open until released",
496            run: |c| {
497                let r = c.run(json!([
498                    {"op":"open","handle":"a","path":"lk_rblock.bin","disposition":"overwrite_if","delete_on_close":true},
499                    {"op":"write","handle":"a","offset":0,"fill_size":100},
500                    {"op":"open","handle":"b","path":"lk_rblock.bin","disposition":"open"},
501                    {"op":"lock","handle":"a","kind":"exclusive","offset":0,"length":50},
502                    {"op":"read","handle":"b","offset":0,"length":50,"expect_fail":true},
503                    {"op":"lock","handle":"a","kind":"unlock","offset":0,"length":50},
504                    {"op":"read","handle":"b","offset":0,"length":50},
505                    {"op":"close","handle":"b"},
506                    {"op":"close","handle":"a"}
507                ]))?;
508                ok(&r)
509            },
510        },
511        // ---- Alternate data streams ([MS-FSCC] §2.6) ----
512        TestCase {
513            id: "streams.roundtrip",
514            category: "Streams",
515            spec: "MS-FSCC §2.6 named streams",
516            about: "Write and read back a named alternate data stream",
517            run: |c| {
518                let r = c.run(json!([
519                    {"op":"open","handle":"base","path":"ads_rt.bin","disposition":"overwrite_if"},
520                    {"op":"write","handle":"base","offset":0,"data":"base"},
521                    {"op":"close","handle":"base"},
522                    {"op":"open","handle":"s","path":"ads_rt.bin:alt","disposition":"overwrite_if"},
523                    {"op":"write","handle":"s","offset":0,"fill_size":256,"fill_seed":13},
524                    {"op":"read","handle":"s","offset":0,"length":256},
525                    {"op":"close","handle":"s"},
526                    {"op":"open","handle":"base2","path":"ads_rt.bin","disposition":"open","delete_on_close":true},
527                    {"op":"close","handle":"base2"}
528                ]))?;
529                ok(&r)?;
530                compare_crc(&r, 4, 5)
531            },
532        },
533        TestCase {
534            id: "streams.enumerate",
535            category: "Streams",
536            spec: "MS-FSCC §2.4.40 FileStreamInformation",
537            about: "Stream enumeration lists the named stream",
538            run: |c| {
539                let r = c.run(json!([
540                    {"op":"open","handle":"base","path":"ads_enum.bin","disposition":"overwrite_if"},
541                    {"op":"write","handle":"base","offset":0,"data":"base"},
542                    {"op":"close","handle":"base"},
543                    {"op":"open","handle":"s","path":"ads_enum.bin:alt","disposition":"overwrite_if"},
544                    {"op":"write","handle":"s","offset":0,"data":"streamdata"},
545                    {"op":"close","handle":"s"},
546                    {"op":"open","handle":"b2","path":"ads_enum.bin","disposition":"open"},
547                    {"op":"query_info","handle":"b2","info_type":"file","info_class":22},
548                    {"op":"close","handle":"b2","delete":true}
549                ]))?;
550                ok(&r)?;
551                let streams = stream_names(&r, 7)?;
552                if !streams.iter().any(|n| n.contains(":alt:")) {
553                    return Err(format!("named stream missing from enumeration: {streams:?}"));
554                }
555                Ok(Metrics::new())
556            },
557        },
558        // ---- Security descriptors ([MS-SMB2] §2.2.37, [MS-FSCC] §2.4.6) ----
559        TestCase {
560            id: "security.query_descriptor",
561            category: "Security",
562            spec: "MS-FSCC §2.4.6 / MS-DTYP §2.4.6",
563            about: "Query owner/group/DACL returns a security descriptor",
564            run: |c| {
565                let r = c.run(json!([
566                    {"op":"open","handle":"f","path":"sd_q.bin","disposition":"overwrite_if","delete_on_close":true},
567                    {"op":"query_info","handle":"f","info_type":"security","info_class":0,"additional":7},
568                    {"op":"close","handle":"f"}
569                ]))?;
570                ok(&r)?;
571                if step_u64(&r, 1, "length")? < 20 {
572                    return Err("security descriptor too small".into());
573                }
574                Ok(Metrics::new())
575            },
576        },
577    ]
578}
579
580/// Open the share root as a directory and close it — a minimal reachability probe.
581fn probe(c: &Ctx, opts: &Opts) -> Result<(), String> {
582    let r = c.run_with(opts, json!([
583        {"op":"open","handle":"d","path":"","directory":true,"disposition":"open"},
584        {"op":"close","handle":"d"}
585    ]))?;
586    ok(&r).map(|_| ())
587}
588
589/// Assert the negotiated dialect string matches.
590fn expect_dialect(c: &Ctx, want: &str) -> Result<(), String> {
591    let r = c.run_with(&Opts::dialect(want), json!([
592        {"op":"open","handle":"d","path":"","directory":true,"disposition":"open"},
593        {"op":"close","handle":"d"}
594    ]))?;
595    ok(&r)?;
596    if r.dialect != want {
597        return Err(format!("negotiated {} but expected {want}", r.dialect));
598    }
599    Ok(())
600}
601
602/// Write a filled pattern and read it back, asserting the crc matches.
603#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] // test I/O sizes
604fn roundtrip(c: &Ctx, opts: &Opts, path: &str, size: u64) -> Result<(), String> {
605    let r = c.run_with(opts, json!([
606        {"op":"open","handle":"f","path":path,"disposition":"overwrite_if","delete_on_close":true},
607        {"op":"write","handle":"f","offset":0,"fill_size":size,"fill_seed":7},
608        {"op":"read","handle":"f","offset":0,"length":size},
609        {"op":"close","handle":"f"}
610    ]))?;
611    ok(&r)?;
612    compare_crc(&r, 1, 2).map(|_| ())
613}
614
615/// Return `Ok` if the driver reported overall success, else the driver error.
616fn ok(r: &crate::DriverResp) -> Result<Metrics, String> {
617    if r.ok {
618        Ok(Metrics::new())
619    } else {
620        Err(r.error.clone().unwrap_or_else(|| "driver reported failure".into()))
621    }
622}
623
624/// Compare the crc32 of a write step against a read step.
625fn compare_crc(r: &crate::DriverResp, write_idx: usize, read_idx: usize) -> Result<Metrics, String> {
626    let w = step_u64(r, write_idx, "crc32")?;
627    let rd = step_u64(r, read_idx, "crc32")?;
628    if w != rd {
629        return Err(format!("data mismatch: write crc {w:#x} != read crc {rd:#x}"));
630    }
631    Ok(Metrics::new())
632}
633
634/// Names returned by a `list` op at the given step index.
635fn list_names(r: &crate::DriverResp, idx: usize) -> Result<Vec<String>, String> {
636    let step = r.step(idx).ok_or("missing list step")?;
637    let arr = step.get("names").and_then(Value::as_array).ok_or("no names in list step")?;
638    Ok(arr.iter().filter_map(|v| v.as_str().map(str::to_string)).collect())
639}
640
641/// Length of the base64 output at a step (decoded byte count).
642#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] // base64 length arithmetic
643fn b64_len(r: &crate::DriverResp, idx: usize) -> Result<usize, String> {
644    let step = r.step(idx).ok_or("missing ioctl step")?;
645    let b64 = step.get("output_b64").and_then(Value::as_str).ok_or("no output_b64")?;
646    Ok(b64.len() * 3 / 4)
647}
648
649/// Stream names from a `query_info` FileStreamInformation step.
650fn stream_names(r: &crate::DriverResp, idx: usize) -> Result<Vec<String>, String> {
651    let step = r.step(idx).ok_or("missing query_info step")?;
652    let arr = step.get("streams").and_then(Value::as_array).ok_or("no streams in step")?;
653    Ok(arr
654        .iter()
655        .filter_map(|v| v.get("name").and_then(Value::as_str).map(str::to_string))
656        .collect())
657}
658
659fn step_u64(r: &crate::DriverResp, idx: usize, key: &str) -> Result<u64, String> {
660    r.step(idx)
661        .and_then(|s| s.get(key))
662        .and_then(Value::as_u64)
663        .ok_or_else(|| format!("step {idx} missing {key}"))
664}