1use serde_json::{json, Value};
9
10use crate::{Ctx, Metrics, Opts, TestCase};
11
12#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] pub fn all() -> Vec<TestCase> {
15 vec![
16 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 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 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 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 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 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 m.insert("bytes".into(), (size * 2) as f64);
171 Ok(m)
172 },
173 },
174 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 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 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 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 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 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 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 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 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
580fn 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
589fn 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#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] fn 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
615fn 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
624fn 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
634fn 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#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] fn 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
649fn 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}