Avoid unnecessary buffer copies

This commit is contained in:
Lucian Petrut
2026-09-09 14:27:30 +00:00
parent 94e63f96ad
commit e3948d2a45
2 changed files with 62 additions and 19 deletions
+60 -12
View File
@@ -132,17 +132,36 @@ def _enable_tcp_nodelay(sock: socket.socket) -> None:
def _recvn(sock: socket.socket, size: int) -> bytes: def _recvn(sock: socket.socket, size: int) -> bytes:
buf = bytearray() buf = bytearray(size)
while len(buf) < size: _recvn_into(sock, memoryview(buf))
chunk = sock.recv(size - len(buf))
if not chunk:
raise NfcProtocolError(
f"NFC connection closed, needed {size} bytes, got {len(buf)}"
)
buf.extend(chunk)
return bytes(buf) return bytes(buf)
def _recvn_into(sock: socket.socket, buf: memoryview) -> None:
"""Read exactly ``len(buf)`` bytes into ``buf``."""
view = buf.cast("B") if buf.format != "B" else buf
filled = 0
n = len(view)
while filled < n:
got = sock.recv_into(view[filled:n])
if not got:
raise NfcProtocolError(
f"NFC connection closed, needed {n} bytes, got {filled}"
)
filled += got
def _writable_bytes(buf: bytearray | memoryview, length: int) -> memoryview:
"""Return a writable ``B`` view of the first ``length`` bytes of ``buf``."""
view = buf if isinstance(buf, memoryview) else memoryview(buf)
if view.readonly:
raise TypeError("read buffer is read-only")
raw = view.cast("B") if view.format != "B" else view
if len(raw) < length:
raise RuntimeError(f"read buffer is {len(raw)} bytes, need {length}")
return raw[:length]
def _send_nfc_msg(sock: socket.socket, msg_type: int, body: bytes = b"") -> None: def _send_nfc_msg(sock: socket.socket, msg_type: int, body: bytes = b"") -> None:
if len(body) > NFC_MSG_SIZE - 4: if len(body) > NFC_MSG_SIZE - 4:
raise ValueError("NFC classic message body too large") raise ValueError("NFC classic message body too large")
@@ -255,9 +274,38 @@ class NfcDisk:
start_sector: Sector offset from the start of the disk. start_sector: Sector offset from the start of the disk.
num_sectors: Number of sectors to read. num_sectors: Number of sectors to read.
""" """
if num_sectors < 1:
raise ValueError("num_sectors must be at least 1")
buf = bytearray(num_sectors * self.sector_size)
self.readinto(start_sector, num_sectors, buf)
return bytes(buf)
def readinto(
self,
start_sector: int,
num_sectors: int,
buf: bytearray | memoryview,
) -> int:
"""Read ``num_sectors`` into ``buf`` starting at ``start_sector``.
Uncompressed fragments are received directly into ``buf``. FastLZ
still decompresses into a temporary buffer, then copies the
result. ``buf`` must be writable and at least
``num_sectors * sector_size`` bytes (a ``get_buffer`` ctypes
array is wrapped with ``memoryview`` by the VDDK-shaped handle).
Args:
start_sector: Sector offset from the start of the disk.
num_sectors: Number of sectors to read.
buf: Destination buffer.
Returns:
The number of bytes written to ``buf``.
"""
if num_sectors < 1: if num_sectors < 1:
raise ValueError("num_sectors must be at least 1") raise ValueError("num_sectors must be at least 1")
length = num_sectors * self.sector_size length = num_sectors * self.sector_size
data = _writable_bytes(buf, length)
offset = start_sector * self.sector_size offset = start_sector * self.sector_size
opcode = NFC_AIO_IO_READ | (self.compression << 32) opcode = NFC_AIO_IO_READ | (self.compression << 32)
payload = struct.pack( payload = struct.pack(
@@ -265,7 +313,6 @@ class NfcDisk:
) )
op_id = self._next_op_id() op_id = self._next_op_id()
self._sock.sendall(_pack_aio_hdr(NFC_AIO_MSG_IO, len(payload), op_id) + payload) self._sock.sendall(_pack_aio_hdr(NFC_AIO_MSG_IO, len(payload), op_id) + payload)
data = bytearray(length)
filled = 0 filled = 0
seen: set[int] = set() seen: set[int] = set()
while filled < length: while filled < length:
@@ -293,6 +340,7 @@ class NfcDisk:
) )
seen.add(dest) seen.add(dest)
ctype = opcode >> 32 ctype = opcode >> 32
chunk_view = data[dest : dest + chunk_len]
if ctype == NFC_COMPRESSION_FASTLZ: if ctype == NFC_COMPRESSION_FASTLZ:
comp_len = struct.unpack_from("<I", body, 36)[0] comp_len = struct.unpack_from("<I", body, 36)[0]
extra = _recvn(self._sock, comp_len) extra = _recvn(self._sock, comp_len)
@@ -306,13 +354,13 @@ class NfcDisk:
raise NfcProtocolError( raise NfcProtocolError(
f"FastLZ read got {len(chunk)} bytes, expected {chunk_len}" f"FastLZ read got {len(chunk)} bytes, expected {chunk_len}"
) )
chunk_view[:] = chunk
elif ctype == NFC_COMPRESSION_NONE: elif ctype == NFC_COMPRESSION_NONE:
chunk = _recvn(self._sock, chunk_len) _recvn_into(self._sock, chunk_view)
else: else:
raise NfcProtocolError(f"unsupported NFC IO compression type {ctype}") raise NfcProtocolError(f"unsupported NFC IO compression type {ctype}")
data[dest : dest + chunk_len] = chunk
filled += chunk_len filled += chunk_len
return bytes(data) return length
def write(self, start_sector: int, num_sectors: int, data: bytes) -> None: def write(self, start_sector: int, num_sectors: int, data: bytes) -> None:
"""Write ``num_sectors`` starting at ``start_sector``. """Write ``num_sectors`` starting at ``start_sector``.
+2 -7
View File
@@ -331,14 +331,9 @@ class VixDiskLibHandle:
start_sector: First sector to read. start_sector: First sector to read.
num_sectors: Number of sectors to read. num_sectors: Number of sectors to read.
buf: Destination buffer (``get_buffer`` or a writable bytes-like). buf: Destination buffer (``get_buffer`` or a writable bytes-like).
Uncompressed NFC extra is received into this buffer.
""" """
data = disk_handle.disk.read(start_sector, num_sectors) disk_handle.disk.readinto(start_sector, num_sectors, memoryview(buf))
if isinstance(buf, (bytearray, memoryview)):
if len(buf) < len(data):
raise RuntimeError(f"read buffer is {len(buf)} bytes, need {len(data)}")
buf[: len(data)] = data
return
ctypes.memmove(buf, data, len(data))
def write( def write(
self, self,