tests/test_smb_transport.py
Rama master · réplica de lectura del repositorio original.
"""SMB 读写传输的隔离行为与请求期限。"""
import errno
import io
import logging
import threading
import unittest
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timezone
from types import SimpleNamespace
from unittest.mock import Mock, patch
from peach.smb_transport import (
SmbConnection, SmbTransport, _DeadlineConnection, _SmbReader,
)
from smbprotocol.exceptions import NoMoreFiles, ObjectNameNotFound, AccessDenied, SMBAuthenticationError
from smbprotocol.file_info import FileAttributes
from smbprotocol.header import NtStatus, SMB2HeaderResponse
from smbprotocol.open import (
CreateDisposition, CreateOptions, Open, ShareAccess, SMB2ReadResponse,
)
def field(value):
return SimpleNamespace(get_value=lambda: value)
class SmbTransportTests(unittest.TestCase):
def setUp(self):
self.config = SmbConnection("files.invalid", "media", "reader", "secret", timeout=.1)
self.conn = Mock()
self.conn.transport.connected = True
self.conn.max_read_size = 4
self.conn.sequence_window = {"high": 20, "low": 0}
self.conn.send.return_value = object()
self.conn.disconnect.side_effect = self.disconnect
self.tree = Mock()
self.tree.is_dfs_share = False
self.tree.session.session_id = 1
self.tree.tree_connect_id = 2
self.patches = [
patch("peach.smb_transport._DeadlineConnection", return_value=self.conn),
patch("peach.smb_transport.smbclient.register_session", return_value=self.tree.session),
patch("peach.smb_transport.TreeConnect", return_value=self.tree),
patch("peach.smb_transport.Open", side_effect=self.fd),
]
self.mocks = [p.start() for p in self.patches]
self.addCleanup(lambda: [p.stop() for p in reversed(self.patches)])
self.transport = SmbTransport(self.config)
self.addCleanup(self.transport.close)
self.fds = []
self.attrs = {}
def disconnect(self, **_):
self.conn.transport.connected = False
def fd(self, tree, name):
fd = Mock()
fd.connection = self.conn
fd.tree_connect = tree
fd.file_name = name
fd.connected = True
fd.end_of_file = 10
fd.file_attributes = self.attrs.get(name, 0)
fd.last_write_time = datetime(2026, 1, 2, 3, 4, 5, 123456, tzinfo=timezone.utc)
fd.close.side_effect = lambda: setattr(fd, "connected", False)
fd.read.side_effect = lambda offset, length, send: (object(), lambda _: b"0123456789"[offset:offset+length])
self.fds.append(fd)
return fd
def test_credentials_are_not_in_connection_repr(self):
self.assertNotIn("reader", repr(self.config))
self.assertNotIn("secret", repr(self.config))
def test_write_uses_explicit_offsets_and_handles_partial_responses(self):
self.conn.max_write_size = 4
with patch.object(self.transport, '_set_info') as set_information:
writer = self.transport.open_write('folder/new.mp4')
fd = self.fds[-1]
fd.write.side_effect = lambda data, offset, send: (object(), lambda _: min(2, len(data)))
self.assertEqual(writer.write(b'abcdef'), 6)
self.assertEqual([call.args[1] for call in fd.write.call_args_list], [0, 2, 4])
self.assertEqual(writer.stat().st_size, 6)
writer.flush()
fd.flush.assert_called_once()
writer.close()
self.assertEqual(set_information.call_args.args[1]['end_of_file'].get_value(), 0)
self.assertTrue(all(not fd.connected for fd in self.fds))
def test_rename_keeps_both_parent_chains_and_refuses_replacement(self):
with patch.object(self.transport, '_set_info') as set_information:
self.transport.rename('a/source.mp4', 'b/target.mp4')
fd, information = set_information.call_args.args
self.assertEqual(fd.file_name, 'a\\source.mp4')
self.assertFalse(information['replace_if_exists'].get_value())
self.assertEqual(information['file_name'].get_value(), 'b\\target.mp4')
self.assertTrue(all(not fd.connected for fd in self.fds))
def test_write_refuses_reparse_before_truncation(self):
self.attrs['link.mp4'] = FileAttributes.FILE_ATTRIBUTE_REPARSE_POINT
with patch.object(self.transport, '_set_info') as set_information:
with self.assertRaises(PermissionError):
self.transport.open_write('link.mp4')
set_information.assert_not_called()
def test_write_permission_failure_and_root_protection(self):
for action in (lambda: self.transport.unlink(()), lambda: self.transport.mkdir(()),
lambda: self.transport.rename('a', ())):
with self.assertRaises(PermissionError):
action()
with patch.object(self.transport, '_set_info', side_effect=AccessDenied()):
with self.assertRaises(PermissionError):
self.transport.unlink('movie.mp4')
self.assertTrue(all(not fd.connected for fd in self.fds))
def test_open_handle_exposes_its_own_size_and_timestamp(self):
with self.transport.open('movie.mp4') as reader:
info = reader.stat()
self.assertEqual(info.st_size, 10)
self.assertGreater(info.st_mtime_ns, 0)
reader.seek(4)
self.assertEqual(reader.stat(), info)
self.assertTrue(all(not fd.connected for fd in self.fds))
def test_invalid_paths_never_connect(self):
for path in ("../x", "/x", "\\x", "C:x", "x/../y", "x//y", "x:stream", "x/", ("a/b",), ("..",), ("x.",)):
with self.subTest(path=path), self.assertRaises(ValueError):
self.transport.open(path)
self.conn.connect.assert_not_called()
def test_stat_preserves_size_time_and_read_sharing(self):
result = self.transport.stat(("folder", "film.mp4"))
self.assertEqual(result.name, "film.mp4")
self.assertEqual(result.st_size, 10)
self.assertEqual(result.mtime_ns % 1_000_000_000, 123456000)
self.assertEqual(result.st_mtime, result.mtime)
self.assertFalse(result.is_dir)
parent, file = self.fds
self.assertEqual(file.create.call_args.args[3], ShareAccess.FILE_SHARE_READ)
self.assertEqual(file.create.call_args.args[4], CreateDisposition.FILE_OPEN)
self.assertTrue(file.create.call_args.args[5] & CreateOptions.FILE_OPEN_REPARSE_POINT)
parent.close.assert_called_once()
file.close.assert_called_once()
def test_parent_reparse_is_rejected_before_child_open(self):
self.attrs["folder"] = FileAttributes.FILE_ATTRIBUTE_REPARSE_POINT
with self.assertRaises(PermissionError):
self.transport.open("folder/film.mp4")
self.assertEqual(len(self.fds), 1)
self.fds[0].close.assert_called_once()
def test_final_reparse_is_rejected(self):
self.attrs["film.mp4"] = FileAttributes.FILE_ATTRIBUTE_REPARSE_POINT
with self.assertRaises(PermissionError):
self.transport.open("film.mp4")
def test_directory_uses_enumeration_metadata_without_per_entry_open(self):
def entries(*_):
def entry(name, attributes=0):
return {key: field(value) for key, value in {
"file_name": name.encode("utf-16-le"), "file_attributes": attributes,
"last_write_time": datetime(2026, 1, 1, tzinfo=timezone.utc), "end_of_file": 12,
}.items()}
return [entry("."), entry("movie.mp4"), entry("folder", FileAttributes.FILE_ATTRIBUTE_DIRECTORY),
entry("link", FileAttributes.FILE_ATTRIBUTE_REPARSE_POINT)]
original = self.mocks[3].side_effect
def directory(tree, name):
fd = original(tree, name)
fd.query_directory.side_effect = [entries(), NoMoreFiles()]
return fd
self.mocks[3].side_effect = directory
result = self.transport.scandir(())
self.assertEqual([x.name for x in result], ["movie.mp4", "folder", "link"])
self.assertEqual(result[0].size, 12)
self.assertTrue(result[1].is_dir)
self.assertTrue(result[2].is_symlink)
self.assertEqual(len(self.fds), 1)
self.assertEqual(self.fds[0].query_directory.call_count, 2)
def test_connection_cache_is_private_and_dfs_is_rejected(self):
self.tree.is_dfs_share = True
with self.assertRaises(PermissionError):
self.transport.stat(())
self.assertTrue(self.transport._closed)
self.assertEqual(self.transport._cache, {})
self.conn.disconnect.assert_called_with(close=False)
def test_missing_and_permission_failures_have_distinct_errno(self):
for exception, expected in ((ObjectNameNotFound(), errno.ENOENT), (AccessDenied(), errno.EACCES),
(SMBAuthenticationError("credential error"), errno.EACCES)):
with self.subTest(expected=expected):
original = self.fd
def bad_fd(tree, name, original=original, exception=exception):
fd = original(tree, name)
fd.create.side_effect = exception
return fd
self.mocks[3].side_effect = bad_fd
with self.assertRaises(OSError) as caught:
self.transport.stat("missing")
self.assertEqual(caught.exception.errno, expected)
def test_seek_short_reads_and_eof(self):
with self.transport.open("film.mp4") as reader:
reader._fd.read.side_effect = lambda offset, length, send: (object(), lambda _: b"0123456789"[offset:offset+min(2, length)])
self.assertEqual(reader.read(5), b"01234")
self.assertEqual(reader.tell(), 5)
self.assertEqual(reader.seek(-2, io.SEEK_CUR), 3)
self.assertEqual(reader.read(2), b"34")
self.assertEqual(reader.seek(-2, io.SEEK_END), 8)
self.assertEqual(reader.read(), b"89")
self.assertEqual(reader.read(), b"")
self.assertEqual(reader.seek(100), 100)
self.assertEqual(reader.read(), b"")
for offset, whence in ((-1, 0), (0, 8)):
with self.assertRaises(ValueError):
reader.seek(offset, whence)
with self.assertRaises(ValueError):
reader.read()
def test_independent_handles_keep_offsets_and_parent_locks(self):
first = self.transport.open("folder/film.mp4")
second = self.transport.open("folder/film.mp4")
self.assertIsNot(first._fd, second._fd)
self.assertEqual(first.read(3), b"012")
self.assertEqual(second.read(2), b"01")
first.close()
self.assertEqual(second.read(2), b"23")
second.close()
self.assertTrue(all(not fd.connected for fd in self.fds))
def test_parallel_handles_read_separate_offsets(self):
first = self.transport.open("film.mp4")
second = self.transport.open("film.mp4")
second.seek(5)
barrier = threading.Barrier(2, timeout=2)
def read_fd(offset, length, send):
def receive(_):
barrier.wait()
return b"0123456789"[offset:offset+length]
return object(), receive
first._fd.read.side_effect = second._fd.read.side_effect = read_fd
with ThreadPoolExecutor() as executor:
a = executor.submit(first.read, 3)
b = executor.submit(second.read, 3)
self.assertEqual(a.result(), b"012")
self.assertEqual(b.result(), b"567")
self.assertEqual((first.tell(), second.tell()), (3, 8))
first.close()
second.close()
def test_premature_empty_read_is_failure_and_disconnects(self):
reader = self.transport.open("film.mp4")
reader._fd.read.side_effect = lambda *args, **kwargs: (object(), lambda _: b"")
with self.assertRaises(OSError) as caught:
reader.read(1)
self.assertEqual(caught.exception.errno, errno.EIO)
self.assertTrue(self.transport._closed)
def test_close_during_read_aborts_connection(self):
reader = self.transport.open("film.mp4")
reader._lock.acquire()
reader.close()
reader._lock.release()
self.assertTrue(reader.closed)
self.conn.disconnect.assert_called_with(close=False)
def test_transport_close_does_not_reconnect(self):
self.transport.stat(())
self.transport.close()
with self.assertRaises(OSError):
self.transport.stat(())
self.conn.connect.assert_called_once()
class SmbDeadlineTests(unittest.TestCase):
def connection(self):
conn = _DeadlineConnection(SmbConnection("files.invalid", "media", timeout=.05))
conn._check_worker_running = Mock()
conn.disconnect = Mock()
return conn
def request(self, status=NtStatus.STATUS_SUCCESS):
response = SMB2HeaderResponse()
response["status"] = status
request = Mock()
request.message = {"message_id": field(1)}
request.response = response
request.response_event = threading.Event()
request.response_event_lock = threading.Lock()
return request
def test_timeout_cancels_and_disconnects_without_background_read(self):
conn, request = self.connection(), self.request()
with self.assertRaises(TimeoutError):
conn.receive(request)
request.cancel.assert_called_once()
conn.disconnect.assert_called_once_with(close=False)
def test_continuous_pending_does_not_extend_deadline(self):
conn, request = self.connection(), self.request(NtStatus.STATUS_PENDING)
request.response_event = Mock()
request.response_event.wait.return_value = True
with patch("peach.smb_transport.time.monotonic", side_effect=[10, 10.01, 10.03, 10.06]):
with self.assertRaises(TimeoutError):
conn.receive(request)
waits = [call.args[0] for call in request.response_event.wait.call_args_list]
self.assertLess(waits[1], waits[0])
request.cancel.assert_called_once()
conn.disconnect.assert_called_once_with(close=False)
def test_cancel_failure_still_disconnects(self):
conn, request = self.connection(), self.request()
request.cancel.side_effect = OSError("offline")
with self.assertRaises(TimeoutError):
conn.receive(request)
conn.disconnect.assert_called_once_with(close=False)
def test_receive_uses_deadline_set_before_send(self):
conn, request = self.connection(), self.request()
request._peach_deadline = 9
with patch("peach.smb_transport.time.monotonic", return_value=10):
with self.assertRaises(TimeoutError):
conn.receive(request)
request.cancel.assert_called_once()
conn.disconnect.assert_called_once_with(close=False)
def test_authentication_uses_deadline_before_tree_connect(self):
conn, request = self.connection(), self.request()
conn.transport = SimpleNamespace(connected=True)
config = SmbConnection("files.invalid", "media", "reader", "secret", timeout=.05)
transport = SmbTransport(config)
with patch("peach.smb_transport._DeadlineConnection", return_value=conn), \
patch.object(conn, "connect"), \
patch("smbclient._pool.Session.connect", autospec=True,
side_effect=lambda session: session.connection.receive(request)), \
patch("peach.smb_transport.TreeConnect") as tree:
with self.assertRaises(TimeoutError):
transport.stat(())
request.cancel.assert_called_once()
tree.assert_not_called()
self.assertTrue(transport._closed)
self.assertEqual(transport._cache, {})
def test_sdk_authentication_details_are_below_logging_threshold(self):
for namespace in ("smbprotocol.session", "smbprotocol.connection", "smbclient._pool"):
with self.subTest(namespace=namespace):
self.assertFalse(logging.getLogger(namespace).isEnabledFor(logging.INFO))
self.assertFalse(logging.getLogger(namespace).isEnabledFor(logging.DEBUG))
def test_response_parser_never_resolves_symlinks(self):
conn, request = self.connection(), self.request()
request.response_event.set()
conn.outstanding_requests[request.message["message_id"].get_value()] = request
# 复用真实库的接收解析路径,只隔离网络事件与线程状态。
self.assertIs(conn.receive(request, resolve_symlinks=True), request.response)
def test_real_open_read_parser_receives_explicit_offset(self):
conn = Mock()
conn.transport.connected = True
conn.max_read_size = 65536
conn.sequence_window = {"high": 10, "low": 0}
tree = SimpleNamespace(session=SimpleNamespace(connection=conn, session_id=7, username="reader"),
tree_connect_id=9, share_name="media")
fd = Open(tree, "film.mp4")
fd.end_of_file = 6
fd.file_id = b"f" * 16
packet = SMB2ReadResponse()
packet["buffer"] = b"def"
response = SMB2HeaderResponse()
response["data"] = packet.pack()
conn.receive.return_value = response
transport = SimpleNamespace(_closed=False, close=Mock())
reader = _SmbReader(transport, fd, [])
reader.seek(3)
self.assertEqual(reader.read(3), b"def")
message = conn.send.call_args.args[0]
self.assertEqual(message["offset"].get_value(), 3)
self.assertEqual(message["length"].get_value(), 3)
self.assertEqual(reader.tell(), 6)
if __name__ == "__main__":
unittest.main()