SL2 Analyzer
Read a FromSoftware .sl2 save and report what is in it
Loading...
Searching...
No Matches
reader.py
Go to the documentation of this file.
1"""Safe, bounds-checked readers.
2
3Nothing anywhere in the package indexes a buffer without going through these. A
4read that would run off the end returns None (or "") instead of raising or
5reading whatever happens to sit past it.
6"""
7
8
9##
10# @brief Read a little-endian unsigned integer, or None if it would run past the
11# end of the buffer.
12# @param buf The bytes to read from.
13# @param off Byte offset. A negative offset is treated as out of range.
14# @param size Width in bytes (1, 2, 4, or 8).
15# @return The integer value, or None if the read is out of range.
16def read_uint(buf, off, size):
17 if off is None or off < 0 or off + size > len(buf):
18 return None
19 return int.from_bytes(buf[off : off + size], "little")
20
21
22## @brief One-byte read. @see read_uint
23def u8(buf, off):
24 return read_uint(buf, off, 1)
25
26
27## @brief Two-byte read. @see read_uint
28def u16(buf, off):
29 return read_uint(buf, off, 2)
30
31
32## @brief Four-byte read. @see read_uint
33def u32(buf, off):
34 return read_uint(buf, off, 4)
35
36
37## @brief Eight-byte read. @see read_uint
38def u64(buf, off):
39 return read_uint(buf, off, 8)
40
41
42##
43# @brief Decode a UTF-16LE string that ends at the first null pair.
44# @details Souls names are UTF-16LE and not always fixed-length, so this reads a
45# bounded window and stops at the first @c 0x0000. Returns an empty string on a
46# bad read rather than raising.
47# @param buf The bytes to read from.
48# @param off Where the string starts.
49# @param max_char Maximum characters to consider.
50# @return The decoded string, stripped of trailing nulls.
51def read_utf16(buf, off, max_char):
52 if off is None or off < 0 or off >= len(buf):
53 return ""
54 raw = buf[off : off + max_char * 2]
55 end = raw.find(b"\x00\x00")
56 if end != -1:
57 raw = raw[: end + (end & 1)] # keep byte pairs aligned
58 try:
59 return raw.decode("utf-16-le", "ignore").rstrip("\x00")
60 except (UnicodeDecodeError, ValueError):
61 return ""
62
63
64## @brief The only characters a real player name may contain. Anything outside
65# this set means the bytes are not a name — usually an empty slot.
66NAME_OK = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 -_'")
67
68
69##
70# @brief Decide whether a decoded string is a plausible character name.
71# @param name The candidate string.
72# @return True if it is non-empty and every character is allowed.
73def is_valid_name(name):
74 return bool(name) and all(c in NAME_OK for c in name)