Skip to content

Commit 4f7d9b0

Browse files
committed
Add tests for FSCTL_QUERY_FILE_REGIONS
1 parent 16cccbe commit 4f7d9b0

File tree

2 files changed

+204
-0
lines changed

2 files changed

+204
-0
lines changed

tests/api2/test_smb_ioctl.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import pytest
2+
import random
3+
4+
from dataclasses import asdict
5+
from middlewared.test.integration.assets.account import user, group
6+
from middlewared.test.integration.assets.pool import dataset
7+
8+
from protocols import smb_connection
9+
from protocols.SMB import (
10+
FsctlQueryFileRegionsRequest,
11+
FileRegionInfo,
12+
FileUsage,
13+
)
14+
15+
from samba import ntstatus
16+
from samba import NTSTATUSError
17+
18+
SHARE_NAME = 'ioctl_share'
19+
20+
21+
@pytest.fixture(scope='module')
22+
def setup_smb_tests(request):
23+
with dataset('smbclient-testing', data={'share_type': 'SMB'}) as ds:
24+
with user({
25+
'username': 'smbuser',
26+
'full_name': 'smbuser',
27+
'group_create': True,
28+
'password': 'Abcd1234'
29+
}) as u:
30+
with smb_share(os.path.join('/mnt', ds), SHARE_NAME) as s:
31+
try:
32+
call('service.start', 'cifs')
33+
yield {'dataset': ds, 'share': s, 'user': u}
34+
finally:
35+
call('service.stop', 'cifs')
36+
37+
38+
def test__query_file_regions_normal(setup_smb_tests):
39+
ds, share, smb_user = setup_smb_tests
40+
with smb_connection(
41+
share=SHARE_NAME,
42+
username=smbuser['username'],
43+
password='Abcd1234',
44+
smb1=False
45+
) as c:
46+
fd = c.create_file("file_regions_normal", "w")
47+
buf = random.randbytes(1024)
48+
49+
for offset in range(0, 128):
50+
c.write(fd, offset=offset * 1024, data=buf)
51+
52+
# First get with region omitted. This should return entire file
53+
fsctl_request_null_region = FsctlQueryFileRegionsRequest(region=None)
54+
fsctl_resp = c.fsctl(fd, fsctl_request_null_region)
55+
56+
assert fsctl_resp.flags == 0
57+
assert fsctl_resp.total_region_entry_count == 1
58+
assert fsctl_resp.region_entry_count == 1
59+
assert fsctl_resp.reserved == 1
60+
assert fsctl_resp.region is not None
61+
62+
assert fsctl_resp.region.offset == 0
63+
assert fsctl_resp.region.length == 128 * 1024
64+
assert fsctl_resp.region.desired_usage == FileUsage.VALID_CACHED_DATA
65+
assert fsctl_resp.region.reserved == 0
66+
67+
# Take same region we retrieved from server and use with new request
68+
fsctl_request_with_region = FsctlQueryFileRegionsRequest(region=fsctl_resp.region)
69+
fsctl_resp2 = c.fsctl(fd, fsctl_request_with_region)
70+
71+
assert asdict(fsctl_resp) == asdict(fsctl_resp2)
72+
73+
74+
def test__query_file_regions_with_holes(setup_smb_tests):
75+
ds, share, smb_user = setup_smb_tests
76+
with smb_connection(
77+
share=SHARE_NAME,
78+
username=smbuser['username'],
79+
password='Abcd1234',
80+
smb1=False
81+
) as c:
82+
fd = c.create_file("file_regions_normal", "w")
83+
buf = random.randbytes(4096)
84+
85+
# insert some holes in file
86+
for offset in range(0, 130):
87+
if offset % 2 == 0:
88+
c.write(fd, offset=offset * 4096, data=buf)
89+
90+
fsctl_request_null_region = FsctlQueryFileRegionsRequest(region=None)
91+
fsctl_resp = c.fsctl(fd, fsctl_request_null_region)
92+
93+
assert fsctl_resp.flags == 0
94+
assert fsctl_resp.total_region_entry_count == 1
95+
assert fsctl_resp.region_entry_count == 1
96+
assert fsctl_resp.reserved == 1
97+
assert fsctl_resp.region is not None
98+
99+
assert fsctl_resp.region.offset == 0
100+
assert fsctl_resp.region.length == 128 * 4096
101+
assert fsctl_resp.region.desired_usage == FileUsage.VALID_CACHED_DATA
102+
assert fsctl_resp.region.reserved == 0
103+
104+
# Take same region we retrieved from server and use with new request
105+
fsctl_request_with_region = FsctlQueryFileRegionsRequest(region=fsctl_resp.region)
106+
fsctl_resp2 = c.fsctl(fd, fsctl_request_with_region)
107+
108+
assert asdict(fsctl_resp) == asdict(fsctl_resp2)
109+
110+
111+
def test__query_file_regions_trailing_zeroes(setup_smb_tests):
112+
"""
113+
FileRegionInfo should contain Valid Data Length which is length in bytes of data
114+
that has been written to the file in the specified region, from the beginning of
115+
the region untile the last byte that has not been zeroed or uninitialized
116+
"""
117+
ds, share, smb_user = setup_smb_tests
118+
with smb_connection(
119+
share=SHARE_NAME,
120+
username=smbuser['username'],
121+
password='Abcd1234',
122+
smb1=False
123+
) as c:
124+
fd = c.create_file("file_regions_normal", "w")
125+
buf = random.randbytes(4096)
126+
127+
# insert a hole in file
128+
c.write(fd, offset=0, data=buf)
129+
c.write(fd, offset=8192, data=buf)
130+
131+
# requesting entire file should give full length
132+
fsctl_request_null_region = FsctlQueryFileRegionsRequest(region=None)
133+
fsctl_resp = c.fsctl(fd, fsctl_request_null_region)
134+
assert fsctl_resp.region.length == 12288
135+
136+
# requesting region that has hole at end of it should only give data length
137+
limited_region = FileRegionInfo(offset=0, length=8192)
138+
fsctl_request_limited_region = FsctlQueryFileRegionsRequest(region=limited_region)
139+
fsctl_resp = c.fsctl(fd, fsctl_request_limited_region)
140+
assert fsctl_resp.region.offset == 0
141+
assert fsctl_resp.region.length == 4096

tests/protocols/smb_proto.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import sys
22
import enum
3+
import struct
34
import subprocess
5+
from dataclasses import dataclass
46
from functions import SRVTarget, get_host_ip
57
from platform import system
68

@@ -32,6 +34,60 @@
3234
libsmb_has_rename = 'rename' in dir(libsmb.Conn)
3335

3436

37+
class Fsctl(enum.IntEnum):
38+
QUERY_FILE_REGIONS = 0x00090284
39+
40+
41+
class FileUsage(enum.IntEnum):
42+
VALID_CACHED_DATA = 0x00000001 # NTFS
43+
VALID_NONCACHED_DATA = 0x00000002 # REFS
44+
45+
46+
@dataclass(frozen=True)
47+
class FileRegionInfo:
48+
""" MS-FSCC 2.3.56.1 """
49+
offset: int
50+
length: int
51+
desired_usage: FileUsage = FileUsage.VALID_CACHED_DATA
52+
reserved: int = 0 # by protocol must be zero
53+
54+
55+
@dataclass(frozen=True)
56+
class FsctlQueryFileRegionsReply:
57+
""" MS-FSCC 2.3.56 """
58+
flags: int # by protocol must be zero
59+
total_region_entry_count: int
60+
region_entry_count: int
61+
reserved: int # by protocol must be zero
62+
region: FileRegionInfo
63+
64+
65+
@dataclass(frozen=True)
66+
class FsctlQueryFileRegionsRequest:
67+
""" MS-FSCC 2.3.55 """
68+
region_info: FileRegionInfo | None
69+
70+
def __post_init__(self):
71+
self.fsctl = Fsctl.QUERY_FILE_REGIONS
72+
73+
def pack(self):
74+
if self.region_info is None:
75+
return b''
76+
77+
return struct.pack(
78+
'<qqII',
79+
self.region_info.offset,
80+
self.region_info.length,
81+
self.region_info.desired_usage,
82+
self.region_info.reserved
83+
)
84+
85+
def unpack(self, buf):
86+
unpacked_resp = list(struct.unpack('<IIII', buf[0:16])
87+
unpacked_resp.append(FileRegionInfo(struct.unpack('<qqII', buf[16:])))
88+
return FsctlQueryFileRegionsReply(*unpacked_resp)
89+
90+
3591
class ACLControl(enum.IntFlag):
3692
SEC_DESC_OWNER_DEFAULTED = 0x0001
3793
SEC_DESC_GROUP_DEFAULTED = 0x0002
@@ -353,3 +409,10 @@ def inherit_acl(self, path, action):
353409
cl = subprocess.run(cmd, capture_output=True)
354410
if cl.returncode != 0:
355411
raise RuntimeError(cl.stdout.decode() or cl.stderr.decode())
412+
413+
def fsctl(self, idx, fsctl_request):
414+
resp = self._connection.fsctl(
415+
self._open_files[idx]["fh"], fsctl_request.fsctl, fsctl_request.pack()
416+
)
417+
418+
return fsctl_request.unpack(resp)

0 commit comments

Comments
 (0)