diff --git a/PyMemoryEditor/app/scanner_panel.py b/PyMemoryEditor/app/scanner_panel.py index a5f3415..fb06e08 100644 --- a/PyMemoryEditor/app/scanner_panel.py +++ b/PyMemoryEditor/app/scanner_panel.py @@ -119,6 +119,9 @@ def _build_ui(self) -> None: self._value_edit = QLineEdit() self._value_edit.setPlaceholderText("e.g. 100 or 0x64 or Hello") self._value_edit.returnPressed.connect(self._on_value_submitted) + # For String (UTF-8) the length is dictated by the typed text, so keep + # the (disabled) length field in sync as the user types. + self._value_edit.textChanged.connect(self._on_value_text_changed) value_form.addRow("Value:", self._value_edit) self._second_value_edit = QLineEdit() @@ -249,13 +252,20 @@ def _on_type_changed(self, label: str) -> None: return is_pattern = spec.is_pattern + is_string = spec.pytype is str and not is_pattern # AOB pattern mode reuses the "Value" line for the pattern string and # hides / forces the rest of the value-shape controls (length, # second value, scan-type combo) because none of them apply to # pattern matching. + # + # String (UTF-8) also locks the length field: the buffer width is the + # UTF-8 byte length of the typed text (multi-byte aware), so letting the + # user override it would only allow truncating or over-allocating the + # value they entered. The field stays visible as a read-only readout + # kept in sync by _sync_string_length / _on_value_text_changed. self._length_spin.setEnabled( - spec.accepts_length_override and not is_pattern + spec.accepts_length_override and not is_pattern and not is_string ) if is_pattern: @@ -267,16 +277,21 @@ def _on_type_changed(self, label: str) -> None: if is_pattern: # No meaningful length for an AOB pattern; the scanner derives it. + self._length_spin.setMaximum(1024) self._length_spin.setValue(1) self._length_spin.setSuffix(" bytes") - elif spec.accepts_length_override: - if spec.pytype is bytes: - self._length_spin.setValue(max(4, self._length_spin.value())) - self._length_spin.setSuffix(" bytes") - else: - self._length_spin.setValue(16) - self._length_spin.setSuffix(" chars") + elif is_string: + # Length tracks the typed text — raise the ceiling so long strings + # aren't visually clamped, then mirror the current text's byte size. + self._length_spin.setMaximum(2_147_483_647) + self._length_spin.setSuffix(" bytes") + self._sync_string_length() + elif spec.accepts_length_override: # Byte Array (Hex) + self._length_spin.setMaximum(1024) + self._length_spin.setValue(max(4, self._length_spin.value())) + self._length_spin.setSuffix(" bytes") else: + self._length_spin.setMaximum(1024) self._length_spin.setValue(spec.length) self._length_spin.setSuffix(" bytes") @@ -302,6 +317,23 @@ def _on_type_changed(self, label: str) -> None: # let _refresh_buttons re-evaluate now that the type has flipped. self._refresh_buttons() + def _on_value_text_changed(self, text: str) -> None: + # Only String (UTF-8) derives its length from the value text; every + # other type owns its length field independently. + spec = find_spec(self._type_combo.currentText()) + if spec is not None and spec.pytype is str and not spec.is_pattern: + self._sync_string_length(text) + + def _sync_string_length(self, text: Optional[str] = None) -> None: + """Mirror the UTF-8 byte length of the value text into the length field. + + Matches ``parse_value``'s str rule (byte length, not character count) + so the read-only readout shows exactly the buffer width the scan uses. + """ + if text is None: + text = self._value_edit.text() + self._length_spin.setValue(max(1, len(text.encode("utf-8")))) + def _on_scan_type_changed(self, index: int) -> None: _, scan_type = SCAN_TYPE_CHOICES[index] ranged = scan_type in ( @@ -354,8 +386,13 @@ def _build_request(self, *, with_value: bool = True) -> Optional[ScanRequest]: writeable_only=self._writable_check.isChecked(), ) + # String (UTF-8) ignores the (disabled) length field: pass None so + # parse_value derives the buffer width from the typed text's UTF-8 + # byte length. Byte Array still honours the user-set override. length_override = ( - self._length_spin.value() if spec.accepts_length_override else None + self._length_spin.value() + if spec.accepts_length_override and spec.pytype is not str + else None ) # Increased/Decreased/Changed/Unchanged compare current vs previous and diff --git a/PyMemoryEditor/linux/process.py b/PyMemoryEditor/linux/process.py index f85e0da..1e38be5 100644 --- a/PyMemoryEditor/linux/process.py +++ b/PyMemoryEditor/linux/process.py @@ -6,7 +6,7 @@ from ..enums import ScanTypesEnum from ..process import AbstractProcess from ..process.errors import ClosedProcess -from ..util import resolve_bufflength +from ..util import prepare_write, resolve_bufflength from ..process.module_info import ModuleInfo from ..process.region import MemoryRegion from ..process.thread_info import ThreadInfo @@ -210,9 +210,9 @@ def write_process_memory( value: Union[bool, int, float, str, bytes], ) -> Union[bool, int, float, str, bytes]: self.__require_open() - return write_process_memory( - self.pid, address, pytype, resolve_bufflength(pytype, bufflength), value - ) + w_pytype, w_length, w_value = prepare_write(pytype, bufflength, value) + write_process_memory(self.pid, address, w_pytype, w_length, w_value) + return value def allocate_memory(self, size: int, *, permission=None) -> int: self.__require_open() diff --git a/PyMemoryEditor/macos/process.py b/PyMemoryEditor/macos/process.py index 390d758..666f45c 100644 --- a/PyMemoryEditor/macos/process.py +++ b/PyMemoryEditor/macos/process.py @@ -9,7 +9,7 @@ from ..process.module_info import ModuleInfo from ..process.region import MemoryRegion from ..process.thread_info import ThreadInfo -from ..util import resolve_bufflength +from ..util import prepare_write, resolve_bufflength from .functions import ( allocate_memory, @@ -309,9 +309,9 @@ def write_process_memory( :param value: value to be written. """ self.__require_open() - return write_process_memory( - self.__task, address, pytype, resolve_bufflength(pytype, bufflength), value - ) + w_pytype, w_length, w_value = prepare_write(pytype, bufflength, value) + write_process_memory(self.__task, address, w_pytype, w_length, w_value) + return value def allocate_memory(self, size: int, *, permission=None) -> int: self.__require_open() diff --git a/PyMemoryEditor/process/abstract.py b/PyMemoryEditor/process/abstract.py index 4ea1c3f..a3ec5bb 100644 --- a/PyMemoryEditor/process/abstract.py +++ b/PyMemoryEditor/process/abstract.py @@ -356,13 +356,220 @@ def write_process_memory( :param address: target memory address (ex: 0x006A9EC0). :param pytype: type of value to be written into memory (bool, int, float, str or bytes). - :param bufflength: value size in bytes. For numeric types (int, float, - bool) you may pass None to use the default — int→4, float→8, bool→1. - str and bytes require an explicit size. + :param bufflength: value size in bytes. + + * For numeric types (int, float, bool) it is the exact write width; + pass ``None`` to use the default — int→4, float→8, bool→1. + * For ``str`` / ``bytes`` it is a *minimum* field width, not a hard + cap. The whole value is always written: if its encoded form is + longer than ``bufflength`` every byte is still written (so + ``write(addr, str, 3, "olá")`` writes all 4 UTF-8 bytes instead + of raising — you may count characters, not bytes). If it is + shorter, the field is NUL-padded up to ``bufflength`` (handy to + clear a fixed-size buffer). ``None`` writes exactly the encoded + length. ``str`` is encoded as UTF-8; no NUL terminator is + appended. :param value: value to be written. + :return: the original ``value`` passed in. """ raise NotImplementedError() + # ------------------------------------------------------------------ # + # Typed convenience read/write helpers + # ------------------------------------------------------------------ # + # + # Thin, self-documenting wrappers over ``read_process_memory`` / + # ``write_process_memory`` with the type and byte width baked into the + # method name — ``read_int(addr)`` instead of + # ``read_process_memory(addr, int, 4)``. They add no new capability (the + # generic methods already cover every case) but spell out the exact C + # type the caller wants, so the IDE can offer them by name and beginners + # don't have to remember which width and signedness a type uses. + # + # Widths are FIXED and identical on every platform and target bitness — + # ``long``/``ulong`` are always 4 bytes here (matching Win32 ``LONG``), + # ``longlong``/``ulonglong`` always 8 — so the same call reads the same + # number of bytes regardless of OS. For a platform-/target-dependent + # pointer width use :attr:`pointer_size` with the generic methods. + # + # ``int`` decodes as *signed* (see ``util.convert.get_c_type_of``), so the + # signed family delegates straight to it. The unsigned family reads/writes + # raw bytes and reinterprets them with ``signed=False`` in the target's + # native byte order (``sys.byteorder`` — the convention the rest of the + # library follows, e.g. ``resolve_pointer_chain``). + + def _read_unsigned(self, address: int, size: int) -> int: + raw = self.read_process_memory(address, bytes, size) + return int.from_bytes(raw, sys.byteorder, signed=False) + + def _write_unsigned(self, address: int, size: int, value: int) -> int: + raw = int(value).to_bytes(size, sys.byteorder, signed=False) + self.write_process_memory(address, bytes, size, raw) + return value + + # --- signed integers ---------------------------------------------- # + + def read_char(self, address: int) -> int: + """Read a signed 8-bit integer (1 byte). See :meth:`read_uchar` for unsigned.""" + return self.read_process_memory(address, int, 1) + + def read_short(self, address: int) -> int: + """Read a signed 16-bit integer (2 bytes).""" + return self.read_process_memory(address, int, 2) + + def read_int(self, address: int) -> int: + """Read a signed 32-bit integer (4 bytes).""" + return self.read_process_memory(address, int, 4) + + def read_long(self, address: int) -> int: + """Read a signed 32-bit integer (4 bytes, matching Win32 ``LONG``).""" + return self.read_process_memory(address, int, 4) + + def read_longlong(self, address: int) -> int: + """Read a signed 64-bit integer (8 bytes).""" + return self.read_process_memory(address, int, 8) + + def write_char(self, address: int, value: int) -> int: + """Write a signed 8-bit integer (1 byte). Returns ``value``.""" + self.write_process_memory(address, int, 1, value) + return value + + def write_short(self, address: int, value: int) -> int: + """Write a signed 16-bit integer (2 bytes). Returns ``value``.""" + self.write_process_memory(address, int, 2, value) + return value + + def write_int(self, address: int, value: int) -> int: + """Write a signed 32-bit integer (4 bytes). Returns ``value``.""" + self.write_process_memory(address, int, 4, value) + return value + + def write_long(self, address: int, value: int) -> int: + """Write a signed 32-bit integer (4 bytes, Win32 ``LONG``). Returns ``value``.""" + self.write_process_memory(address, int, 4, value) + return value + + def write_longlong(self, address: int, value: int) -> int: + """Write a signed 64-bit integer (8 bytes). Returns ``value``.""" + self.write_process_memory(address, int, 8, value) + return value + + # --- unsigned integers -------------------------------------------- # + + def read_uchar(self, address: int) -> int: + """Read an unsigned 8-bit integer (1 byte, 0..255).""" + return self._read_unsigned(address, 1) + + def read_ushort(self, address: int) -> int: + """Read an unsigned 16-bit integer (2 bytes).""" + return self._read_unsigned(address, 2) + + def read_uint(self, address: int) -> int: + """Read an unsigned 32-bit integer (4 bytes).""" + return self._read_unsigned(address, 4) + + def read_ulong(self, address: int) -> int: + """Read an unsigned 32-bit integer (4 bytes, matching Win32 ``ULONG``).""" + return self._read_unsigned(address, 4) + + def read_ulonglong(self, address: int) -> int: + """Read an unsigned 64-bit integer (8 bytes).""" + return self._read_unsigned(address, 8) + + def write_uchar(self, address: int, value: int) -> int: + """Write an unsigned 8-bit integer (1 byte). Returns ``value``.""" + return self._write_unsigned(address, 1, value) + + def write_ushort(self, address: int, value: int) -> int: + """Write an unsigned 16-bit integer (2 bytes). Returns ``value``.""" + return self._write_unsigned(address, 2, value) + + def write_uint(self, address: int, value: int) -> int: + """Write an unsigned 32-bit integer (4 bytes). Returns ``value``.""" + return self._write_unsigned(address, 4, value) + + def write_ulong(self, address: int, value: int) -> int: + """Write an unsigned 32-bit integer (4 bytes, Win32 ``ULONG``). Returns ``value``.""" + return self._write_unsigned(address, 4, value) + + def write_ulonglong(self, address: int, value: int) -> int: + """Write an unsigned 64-bit integer (8 bytes). Returns ``value``.""" + return self._write_unsigned(address, 8, value) + + # --- floating point ----------------------------------------------- # + + def read_float(self, address: int) -> float: + """Read a 32-bit IEEE-754 float (4 bytes).""" + return self.read_process_memory(address, float, 4) + + def read_double(self, address: int) -> float: + """Read a 64-bit IEEE-754 double (8 bytes).""" + return self.read_process_memory(address, float, 8) + + def write_float(self, address: int, value: float) -> float: + """Write a 32-bit IEEE-754 float (4 bytes). Returns ``value``.""" + self.write_process_memory(address, float, 4, value) + return value + + def write_double(self, address: int, value: float) -> float: + """Write a 64-bit IEEE-754 double (8 bytes). Returns ``value``.""" + self.write_process_memory(address, float, 8, value) + return value + + # --- boolean ------------------------------------------------------- # + + def read_bool(self, address: int) -> bool: + """Read a boolean (1 byte).""" + return self.read_process_memory(address, bool, 1) + + def write_bool(self, address: int, value: bool) -> bool: + """Write a boolean (1 byte). Returns ``value``.""" + self.write_process_memory(address, bool, 1, value) + return value + + # --- strings & raw bytes ------------------------------------------ # + + def read_string(self, address: int, byte_count: int) -> str: + """ + Read up to ``byte_count`` bytes, decode them as UTF-8 and return the + text up to the first NUL terminator (C-string semantics). + + Goes through the ``str`` read path, so invalid UTF-8 becomes ``U+FFFD`` + (``errors="replace"``). ``byte_count`` is the maximum field width to + read; the NUL terminator and everything after it are dropped. To make a + shorter :meth:`write_string` read back cleanly here, write it with + ``null_terminator=True`` (or into an already-zeroed field). + """ + return self.read_process_memory(address, str, byte_count).split("\x00", 1)[0] + + def write_string( + self, address: int, text: str, *, null_terminator: bool = False + ) -> str: + """ + Write ``text`` as a UTF-8 string, writing exactly its bytes and nothing + else. Pass ``null_terminator=True`` to also append a trailing ``\\x00`` + — useful when overwriting a longer string in place so :meth:`read_string` + stops where you intend rather than reading the stale tail. + + Multi-byte characters are handled correctly — the field grows to the + encoded byte length, so you never have to count bytes yourself. + Returns ``text``. For a fixed-width / NUL-padded field, call + :meth:`write_process_memory` with ``pytype=str`` and an explicit + ``bufflength`` instead. + """ + payload = text + "\x00" if null_terminator else text + self.write_process_memory(address, str, None, payload) + return text + + def read_bytes(self, address: int, length: int) -> bytes: + """Read ``length`` raw bytes verbatim (no decoding).""" + return self.read_process_memory(address, bytes, length) + + def write_bytes(self, address: int, data: bytes) -> bytes: + """Write the raw byte string ``data`` verbatim. Returns ``data``.""" + self.write_process_memory(address, bytes, len(data), data) + return data + @abstractmethod def allocate_memory(self, size: int, *, permission=None) -> int: """ diff --git a/PyMemoryEditor/util/__init__.py b/PyMemoryEditor/util/__init__.py index 12d3af1..03bd953 100644 --- a/PyMemoryEditor/util/__init__.py +++ b/PyMemoryEditor/util/__init__.py @@ -4,6 +4,7 @@ _validate_pytype, convert_from_byte_array, get_c_type_of, + prepare_write, resolve_bufflength, value_to_bytes, values_to_bytes, diff --git a/PyMemoryEditor/util/convert.py b/PyMemoryEditor/util/convert.py index 566b047..ae4e665 100644 --- a/PyMemoryEditor/util/convert.py +++ b/PyMemoryEditor/util/convert.py @@ -49,6 +49,49 @@ def resolve_bufflength(pytype: Type, bufflength: Optional[int]) -> int: ) +def prepare_write( + pytype: Type, bufflength: Optional[int], value +) -> Tuple[Type, int, Any]: + """ + Normalize a write request into the ``(pytype, length, value)`` triple a + backend ``WriteProcessMemory`` can hand straight to the OS. + + * **Numeric / bool** — returned unchanged, with ``bufflength`` resolved to + its default (see :func:`resolve_bufflength`). The backend encodes the + value via :func:`get_c_type_of` exactly as before. + + * **str / bytes** — the value is encoded to raw bytes (UTF-8 for ``str``) + and routed through the ``bytes`` path. Here ``bufflength`` is a + *minimum* field width, not a hard cap: + + - the **whole** value is always written, even when its encoded form is + longer than ``bufflength`` — so ``write(addr, str, 3, "olá")`` writes + all 4 UTF-8 bytes instead of raising ``ValueError`` because the caller + counted characters, not bytes; + - when the encoded form is **shorter** than ``bufflength`` the buffer is + NUL-padded up to it, which lets you clear a fixed-size field + (``write(addr, str, 16, "AB")`` writes ``b"AB"`` + 14 zero bytes); + - ``bufflength=None`` writes exactly the encoded length. + + The caller is expected to return its *original* ``value`` to the user, so + this routing through ``bytes`` stays invisible at the public API. + """ + _validate_pytype(pytype) + + if pytype is str or pytype is bytes: + raw = value.encode("utf-8") if isinstance(value, str) else value + if not isinstance(raw, (bytes, bytearray)): + raise TypeError( + "value must be str or bytes when pytype is str/bytes, got %s." + % type(value).__name__ + ) + raw = bytes(raw) + width = len(raw) if bufflength is None else max(bufflength, len(raw)) + return bytes, width, raw.ljust(width, b"\x00") + + return pytype, resolve_bufflength(pytype, bufflength), value + + def convert_from_byte_array( byte_array: ctypes.Array, pytype: Type[T], length: int ) -> T: diff --git a/PyMemoryEditor/win32/process.py b/PyMemoryEditor/win32/process.py index 5ee8de4..87595da 100644 --- a/PyMemoryEditor/win32/process.py +++ b/PyMemoryEditor/win32/process.py @@ -3,7 +3,7 @@ import ctypes from typing import Dict, Generator, Optional, Sequence, Tuple, Type, TypeVar, Union -from ..util import resolve_bufflength +from ..util import prepare_write, resolve_bufflength from ..enums import ScanTypesEnum from ..process import AbstractProcess @@ -313,13 +313,15 @@ def write_process_memory( ) -> Union[bool, int, float, str, bytes]: self.__require_open() self.__require_write() - return WriteProcessMemory( + w_pytype, w_length, w_value = prepare_write(pytype, bufflength, value) + WriteProcessMemory( self.__process_handle, address, - pytype, - resolve_bufflength(pytype, bufflength), - value, + w_pytype, + w_length, + w_value, ) + return value def allocate_memory(self, size: int, *, permission=None) -> int: self.__require_open() diff --git a/README.md b/README.md index 9103cbf..0ef2d43 100644 --- a/README.md +++ b/README.md @@ -69,8 +69,9 @@ with OpenProcess(process_name="game.exe") as process: for address in process.search_by_value(int, 4, 100): print(f"Found at 0x{address:X}") - # Write a new value at a known address. - process.write_process_memory(0x006A9EC0, int, 4, 9999) + # Read the current value, then write a new one back. + current = process.read_int(address) + process.write_int(address, current + 500) ``` That's it — read, write or scan another process in three lines, the same way on every platform. diff --git a/docs/api/openprocess.md b/docs/api/openprocess.md index 806f575..abc7baa 100644 --- a/docs/api/openprocess.md +++ b/docs/api/openprocess.md @@ -110,10 +110,78 @@ with OpenProcess( :param int address: target memory address. :param Type pytype: one of the five supported types. :param int bufflength: value size in bytes (``None`` for numeric defaults). + For ``str`` / ``bytes`` it is a *minimum* width — the whole value is + always written, and a larger size zero-pads the field. :param value: the value to write. :returns: the written value. ``` +### Typed shortcuts + +Convenience `read_*` / `write_*` pairs with the size and signedness baked into +the name — see :doc:`../guide/read-write` for examples. Widths are fixed and +identical on every platform. + +```{eval-rst} +.. py:method:: read_char(address) + :no-index: + + Read / write a signed 8-bit integer (1 byte). Pair: ``write_char(address, value)``. + +.. py:method:: read_short(address) + :no-index: + + Signed 16-bit integer (2 bytes). Pair: ``write_short``. + +.. py:method:: read_int(address) + :no-index: + + Signed 32-bit integer (4 bytes). Pair: ``write_int``. + +.. py:method:: read_long(address) + :no-index: + + Signed 32-bit integer (4 bytes, Win32 ``LONG``). Pair: ``write_long``. + +.. py:method:: read_longlong(address) + :no-index: + + Signed 64-bit integer (8 bytes). Pair: ``write_longlong``. + +.. py:method:: read_uchar(address) + :no-index: + + Unsigned variants of the above: ``read_uchar`` / ``read_ushort`` / + ``read_uint`` / ``read_ulong`` / ``read_ulonglong`` (1 / 2 / 4 / 4 / 8 bytes), + each with a matching ``write_*``. + +.. py:method:: read_float(address) + :no-index: + + 32-bit float (4 bytes). Pair: ``write_float``. + +.. py:method:: read_double(address) + :no-index: + + 64-bit double (8 bytes). Pair: ``write_double``. + +.. py:method:: read_bool(address) + :no-index: + + Boolean (1 byte). Pair: ``write_bool``. + +.. py:method:: read_string(address, byte_count) + :no-index: + + Read up to ``byte_count`` bytes, decode UTF-8, return the text up to the + first NUL. Pair: ``write_string(address, text, *, null_terminator=False)``. + +.. py:method:: read_bytes(address, length) + :no-index: + + Read ``length`` raw bytes. Pair: ``write_bytes(address, data)``. +``` + ### Searching ```{eval-rst} diff --git a/docs/guide/read-write.md b/docs/guide/read-write.md index a8a1520..6219a9e 100644 --- a/docs/guide/read-write.md +++ b/docs/guide/read-write.md @@ -19,7 +19,15 @@ process memory: For numeric types, you can pass `bufflength=None` (or just omit it) to use the -default. For `str` and `bytes`, you **must** pass the size. +default. For `str` and `bytes`, the size is **required when reading** (the +library needs to know how many bytes to pull back) but **optional when +writing** — a write simply stores the value you gave it. + +```{tip} +Prefer the **typed shortcuts** below (`read_int`, `write_float`, `read_string`…) +if you don't want to think about sizes at all — the width is baked into the +method name. +``` ## Reading a value @@ -78,13 +86,23 @@ with OpenProcess(process_name="notepad.exe") as process: # Write a 2-byte int explicitly process.write_process_memory(address, int, 2, 42) - # Write a string - process.write_process_memory(address, str, 32, "Hello!") + # Write a string — the size is optional; None just stores your text as-is + process.write_process_memory(address, str, None, "Hello!") # Write raw bytes process.write_process_memory(address, bytes, 4, b"\xDE\xAD\xBE\xEF") ``` +```{admonition} Writing text? Count characters, not bytes. +:class: tip + +For `str` / `bytes` writes, `bufflength` is just a **minimum** width — your +value is always written in full. So `write_process_memory(addr, str, 3, "olá")` +writes all of `"olá"` even though the `á` takes 2 bytes (4 bytes total): you can +think in characters and never worry about UTF-8 byte math. Pass a *larger* +size to clear a fixed-size field (the extra space is zero-filled). +``` + ### Method signature ```{eval-rst} @@ -99,13 +117,82 @@ with OpenProcess(process_name="notepad.exe") as process: :return: the written value. ``` +## Typed shortcuts + +Don't want to remember that an *Int32* is 4 bytes or that *unsigned* needs +special handling? Use the **typed shortcuts**. Each one is a `read_*` / +`write_*` pair with the size and signedness baked into the name: + +```python +with OpenProcess(process_name="game.exe") as process: + hp = process.read_int(0x7FF40010) # signed, 4 bytes + gold = process.read_uint(0x7FF40014) # unsigned, 4 bytes + speed = process.read_float(0x7FF40018) # 32-bit float + + process.write_int(0x7FF40010, hp + 100) # heal up + process.write_bool(0x7FF4001C, True) # toggle a flag +``` + +No `bufflength`, no `pytype` — just the address (and the value, when writing). +Here's the full set; every `read_*` has a matching `write_*`: + +
| Shortcut | Reads / writes | Bytes |
|---|---|---|
read_char / read_uchar | 8-bit integer — signed / unsigned | 1 |
read_short / read_ushort | 16-bit integer — signed / unsigned | 2 |
read_int / read_uint | 32-bit integer — signed / unsigned | 4 |
read_long / read_ulong | 32-bit integer — signed / unsigned | 4 |
read_longlong / read_ulonglong | 64-bit integer — signed / unsigned | 8 |
read_float | 32-bit floating point | 4 |
read_double | 64-bit floating point | 8 |
read_bool | boolean | 1 |
read_string / read_bytes | text / raw bytes | you choose |