PyLibMS 4.3.4__py3-none-any.whl → 5.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lms/common/field/io.py +13 -15
- lms/common/field/lms_datatype.py +35 -42
- lms/common/field/lms_field.py +34 -30
- lms/common/lms_constants.py +1 -0
- lms/common/stream/section.py +9 -13
- lms/fileio/io.py +62 -63
- lms/flowchart/definitions/node.py +53 -21
- lms/flowchart/flw3.py +26 -8
- lms/flowchart/msbf.py +65 -51
- lms/flowchart/msbfio.py +22 -15
- lms/message/lms_messagetext.py +15 -13
- lms/message/msbt.py +39 -21
- lms/message/msbtio.py +51 -18
- lms/message/tag/io/param_io.py +11 -8
- lms/message/tag/io/tag_io.py +12 -12
- lms/message/tag/lms_tag.py +19 -20
- lms/project/definitions/attribute.py +14 -6
- lms/project/definitions/tag.py +79 -35
- lms/project/msbp.py +12 -12
- lms/project/msbpread.py +26 -28
- lms/project/section/ali2.py +10 -7
- lms/project/section/ati2.py +5 -6
- lms/project/section/clr1.py +1 -1
- lms/project/section/string.py +5 -7
- lms/project/section/syl3.py +4 -4
- lms/project/section/tag2.py +4 -4
- lms/project/section/tgg2.py +3 -3
- lms/project/section/tgp2.py +7 -7
- lms/py.typed +0 -0
- lms/titleconfig/config.py +31 -88
- lms/titleconfig/definitions/attribute.py +23 -7
- lms/titleconfig/definitions/field.py +32 -0
- lms/titleconfig/definitions/nodes.py +68 -64
- lms/titleconfig/definitions/tags.py +45 -48
- {pylibms-4.3.4.dist-info → pylibms-5.0.0.dist-info}/METADATA +20 -27
- {pylibms-4.3.4.dist-info → pylibms-5.0.0.dist-info}/RECORD +39 -39
- lms/flowchart/definitions/flowchart.py +0 -100
- lms/titleconfig/definitions/value.py +0 -18
- {pylibms-4.3.4.dist-info → pylibms-5.0.0.dist-info}/WHEEL +0 -0
- {pylibms-4.3.4.dist-info → pylibms-5.0.0.dist-info}/licenses/LICENSE +0 -0
- {pylibms-4.3.4.dist-info → pylibms-5.0.0.dist-info}/top_level.txt +0 -0
lms/common/field/io.py
CHANGED
|
@@ -4,18 +4,18 @@ from lms.common.field.lms_datatype import (
|
|
|
4
4
|
LMS_DataType,
|
|
5
5
|
is_bool_datatype,
|
|
6
6
|
is_bytes_datatype,
|
|
7
|
-
|
|
7
|
+
is_enum_datatype,
|
|
8
8
|
is_number_datatype,
|
|
9
9
|
)
|
|
10
10
|
from lms.common.field.lms_field import LMS_Field
|
|
11
11
|
from lms.fileio.io import FileReader, FileWriter
|
|
12
|
-
from lms.titleconfig.definitions.
|
|
12
|
+
from lms.titleconfig.definitions.field import FieldDefinition
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
def read_field(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
reader: FileReader,
|
|
17
|
+
definition: FieldDefinition,
|
|
18
|
+
read_variable_width_value: Callable | None = None,
|
|
19
19
|
) -> LMS_Field:
|
|
20
20
|
# String is excluded as their reading varies between MSBT/MSBF
|
|
21
21
|
match definition.datatype:
|
|
@@ -33,27 +33,25 @@ def read_field(
|
|
|
33
33
|
value = reader.read_int32()
|
|
34
34
|
case LMS_DataType.FLOAT32:
|
|
35
35
|
value = reader.read_float32()
|
|
36
|
-
case LMS_DataType.
|
|
36
|
+
case LMS_DataType.ENUM:
|
|
37
37
|
if read_variable_width_value is not None:
|
|
38
38
|
index = read_variable_width_value()
|
|
39
39
|
else:
|
|
40
40
|
index = reader.read_uint8()
|
|
41
|
-
value = definition.
|
|
41
|
+
value = definition.enum_members[index]
|
|
42
42
|
case LMS_DataType.BOOL:
|
|
43
43
|
if read_variable_width_value is not None:
|
|
44
44
|
value = bool(read_variable_width_value())
|
|
45
45
|
else:
|
|
46
46
|
value = bool(reader.read_uint8())
|
|
47
|
-
case LMS_DataType.BYTES:
|
|
48
|
-
value = reader.read_bytes(1)
|
|
49
47
|
|
|
50
|
-
return LMS_Field(value, definition)
|
|
48
|
+
return LMS_Field(value, definition)
|
|
51
49
|
|
|
52
50
|
|
|
53
51
|
def write_field(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
writer: FileWriter,
|
|
53
|
+
field: LMS_Field,
|
|
54
|
+
write_variable_width_value: Callable | None = None,
|
|
57
55
|
) -> None:
|
|
58
56
|
if is_number_datatype(field.value, field.datatype):
|
|
59
57
|
write_functions: dict[LMS_DataType, Callable] = {
|
|
@@ -68,8 +66,8 @@ def write_field(
|
|
|
68
66
|
write_functions[field.datatype](field.value)
|
|
69
67
|
return
|
|
70
68
|
|
|
71
|
-
if
|
|
72
|
-
value = field.
|
|
69
|
+
if is_enum_datatype(field.value, field.datatype):
|
|
70
|
+
value = list(field.enum_members.values()).index(field.value)
|
|
73
71
|
if write_variable_width_value is not None:
|
|
74
72
|
write_variable_width_value(value)
|
|
75
73
|
else:
|
lms/common/field/lms_datatype.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from enum import
|
|
3
|
+
from enum import IntEnum, auto
|
|
4
4
|
from typing import Type, TypeGuard
|
|
5
5
|
|
|
6
6
|
FLOAT32_MIN = -3.4028235e38
|
|
@@ -15,8 +15,9 @@ ALIAS_MAP = {
|
|
|
15
15
|
"i32": "INT32",
|
|
16
16
|
"f32": "FLOAT32",
|
|
17
17
|
"str": "STRING",
|
|
18
|
+
# LIST fallback for ENUM datatype
|
|
19
|
+
"list": "ENUM",
|
|
18
20
|
"bool": "BOOL",
|
|
19
|
-
"byte": "BYTES",
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
|
|
@@ -32,8 +33,8 @@ def is_number_datatype(value: object, datatype: LMS_DataType) -> TypeGuard[int |
|
|
|
32
33
|
) and isinstance(value, int)
|
|
33
34
|
|
|
34
35
|
|
|
35
|
-
def
|
|
36
|
-
return datatype is LMS_DataType.
|
|
36
|
+
def is_enum_datatype(value: object, datatype: LMS_DataType) -> TypeGuard[str]:
|
|
37
|
+
return datatype is LMS_DataType.ENUM and isinstance(value, str)
|
|
37
38
|
|
|
38
39
|
|
|
39
40
|
def is_bool_datatype(value: object, datatype: LMS_DataType) -> TypeGuard[bool]:
|
|
@@ -48,7 +49,7 @@ def is_string_datatype(value: object, datatype: LMS_DataType) -> TypeGuard[str]:
|
|
|
48
49
|
return datatype is LMS_DataType.STRING and isinstance(value, str)
|
|
49
50
|
|
|
50
51
|
|
|
51
|
-
class LMS_DataType(
|
|
52
|
+
class LMS_DataType(IntEnum):
|
|
52
53
|
"""Enum that represents a datatype for a value entry in a MSBT/MSBP file."""
|
|
53
54
|
|
|
54
55
|
UINT8 = 0
|
|
@@ -62,64 +63,56 @@ class LMS_DataType(Enum):
|
|
|
62
63
|
FLOAT32 = 6
|
|
63
64
|
|
|
64
65
|
STRING = 8
|
|
65
|
-
|
|
66
|
+
ENUM = 9
|
|
66
67
|
|
|
67
|
-
#
|
|
68
|
-
|
|
69
|
-
# The actual value isn't important since they are not real types, but are instantiated from a config
|
|
70
|
-
BOOL = "bool"
|
|
71
|
-
BYTES = "byte"
|
|
72
|
-
|
|
73
|
-
def to_string(self) -> str:
|
|
74
|
-
return self._name_.lower()
|
|
68
|
+
# Interface type for UINT8
|
|
69
|
+
BOOL = auto()
|
|
75
70
|
|
|
76
71
|
@property
|
|
77
72
|
def signed(self) -> bool:
|
|
78
73
|
"""Property for if the type is signed or not."""
|
|
79
74
|
if self not in [
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
LMS_DataType.BYTES,
|
|
75
|
+
self.STRING,
|
|
76
|
+
self.ENUM,
|
|
77
|
+
self.BOOL,
|
|
84
78
|
]:
|
|
85
|
-
return self in [
|
|
79
|
+
return self in [self.INT8, self.INT16, self.INT32]
|
|
86
80
|
|
|
87
|
-
raise TypeError(f"Signed is not a valid property for '{self.
|
|
81
|
+
raise TypeError(f"Signed is not a valid property for '{self.name.lower()}'!")
|
|
88
82
|
|
|
89
83
|
@property
|
|
90
84
|
def builtin_type(self) -> Type[int | float | str | bool | bytes]:
|
|
91
85
|
"""The enum as the builtin python type."""
|
|
92
86
|
return {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
LMS_DataType.BYTES: bytes,
|
|
87
|
+
self.UINT8: int,
|
|
88
|
+
self.UINT16: int,
|
|
89
|
+
self.UINT32: int,
|
|
90
|
+
self.INT8: int,
|
|
91
|
+
self.INT16: int,
|
|
92
|
+
self.INT32: int,
|
|
93
|
+
self.FLOAT32: float,
|
|
94
|
+
self.STRING: str,
|
|
95
|
+
self.ENUM: str,
|
|
96
|
+
self.BOOL: bool,
|
|
104
97
|
}[self]
|
|
105
98
|
|
|
106
99
|
@property
|
|
107
100
|
def stream_size(self) -> int:
|
|
108
101
|
"""Size of the datatype, only for fixed integers."""
|
|
109
102
|
sizes = {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
103
|
+
self.UINT8: 1,
|
|
104
|
+
self.UINT16: 2,
|
|
105
|
+
self.UINT32: 4,
|
|
106
|
+
self.INT8: 1,
|
|
107
|
+
self.INT16: 2,
|
|
108
|
+
self.INT32: 4,
|
|
109
|
+
self.FLOAT32: 4,
|
|
117
110
|
}
|
|
118
111
|
|
|
119
112
|
try:
|
|
120
113
|
return sizes[self]
|
|
121
114
|
except KeyError:
|
|
122
|
-
raise TypeError(f"Stream size is not defined for '{self.
|
|
115
|
+
raise TypeError(f"Stream size is not defined for '{self.name.lower()}'.")
|
|
123
116
|
|
|
124
117
|
@classmethod
|
|
125
118
|
def from_string(cls, string: str):
|
|
@@ -136,8 +129,8 @@ class LMS_DataType(Enum):
|
|
|
136
129
|
|
|
137
130
|
|
|
138
131
|
def verify_number_from_datatype(
|
|
139
|
-
|
|
140
|
-
|
|
132
|
+
value: int | float,
|
|
133
|
+
datatype: LMS_DataType,
|
|
141
134
|
):
|
|
142
135
|
if datatype is LMS_DataType.FLOAT32:
|
|
143
136
|
max_value = FLOAT32_MAX
|
|
@@ -148,7 +141,7 @@ def verify_number_from_datatype(
|
|
|
148
141
|
max_value = 2 ** (bits - 1)
|
|
149
142
|
min_value = -max_value
|
|
150
143
|
else:
|
|
151
|
-
min_value, max_value = 0, (2**bits) - 1
|
|
144
|
+
min_value, max_value = 0, (2 ** bits) - 1
|
|
152
145
|
|
|
153
146
|
if not min_value <= value <= max_value:
|
|
154
147
|
raise ValueError(
|
lms/common/field/lms_field.py
CHANGED
|
@@ -1,21 +1,32 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from collections.abc import Iterator
|
|
4
|
-
from
|
|
4
|
+
from types import MappingProxyType
|
|
5
|
+
from typing import Mapping, TypeAlias
|
|
5
6
|
|
|
6
7
|
from lms.common.field.lms_datatype import LMS_DataType, verify_number_from_datatype
|
|
7
|
-
from lms.titleconfig.definitions.
|
|
8
|
+
from lms.titleconfig.definitions.field import FieldDefinition
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
FieldValue: TypeAlias = int | str | float | bool | bytes
|
|
10
11
|
|
|
11
12
|
|
|
12
|
-
@dataclass(frozen=True)
|
|
13
13
|
class LMS_FieldMap:
|
|
14
14
|
"""
|
|
15
|
-
|
|
15
|
+
An immutable wrapper for a basic ``Mapping[str, LMS_Field]``.
|
|
16
|
+
|
|
17
|
+
Fields are validated by their datatype upon modification.
|
|
16
18
|
"""
|
|
17
19
|
|
|
18
|
-
fields:
|
|
20
|
+
def __init__(self, fields: Mapping[str, LMS_Field]) -> None:
|
|
21
|
+
self._fields = MappingProxyType(fields)
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def fields(self) -> Mapping[str, LMS_Field]:
|
|
25
|
+
"""The fields associated to this LMS_FieldMap."""
|
|
26
|
+
return self._fields
|
|
27
|
+
|
|
28
|
+
def __len__(self) -> int:
|
|
29
|
+
return len(self.fields)
|
|
19
30
|
|
|
20
31
|
def __iter__(self) -> Iterator[LMS_Field]:
|
|
21
32
|
return iter(self.fields.values())
|
|
@@ -38,15 +49,15 @@ class LMS_FieldMap:
|
|
|
38
49
|
|
|
39
50
|
@staticmethod
|
|
40
51
|
def create_default_dict_map(
|
|
41
|
-
|
|
52
|
+
definitions: list[FieldDefinition],
|
|
42
53
|
) -> dict[str, FieldValue]:
|
|
43
54
|
map = {}
|
|
44
55
|
for definition in definitions:
|
|
45
56
|
match definition.datatype:
|
|
46
57
|
case LMS_DataType.STRING:
|
|
47
58
|
map[definition.name] = ""
|
|
48
|
-
case LMS_DataType.
|
|
49
|
-
map[definition.name] = definition.
|
|
59
|
+
case LMS_DataType.ENUM:
|
|
60
|
+
map[definition.name] = definition.enum_members[0]
|
|
50
61
|
case LMS_DataType.BOOL:
|
|
51
62
|
map[definition.name] = False
|
|
52
63
|
case LMS_DataType.FLOAT32:
|
|
@@ -56,11 +67,11 @@ class LMS_FieldMap:
|
|
|
56
67
|
return map
|
|
57
68
|
|
|
58
69
|
@classmethod
|
|
59
|
-
def create_default_map(cls, definitions: list[
|
|
70
|
+
def create_default_map(cls, definitions: list[FieldDefinition]):
|
|
60
71
|
return cls.from_dict(LMS_FieldMap.create_default_map(definitions), definitions)
|
|
61
72
|
|
|
62
73
|
@classmethod
|
|
63
|
-
def from_dict(cls, data: dict[str, FieldValue], definitions: list[
|
|
74
|
+
def from_dict(cls, data: dict[str, FieldValue], definitions: list[FieldDefinition]):
|
|
64
75
|
return cls(
|
|
65
76
|
{
|
|
66
77
|
definition.name: LMS_Field(data[definition.name], definition)
|
|
@@ -69,20 +80,18 @@ class LMS_FieldMap:
|
|
|
69
80
|
)
|
|
70
81
|
|
|
71
82
|
@classmethod
|
|
72
|
-
def from_string_dict(cls, data: dict[str, str], definitions: list[
|
|
83
|
+
def from_string_dict(cls, data: dict[str, str], definitions: list[FieldDefinition]):
|
|
73
84
|
fields = {}
|
|
74
85
|
casted_value = None
|
|
75
86
|
|
|
76
87
|
for definition in definitions:
|
|
77
88
|
# These datatypes do not require any casting
|
|
78
|
-
if definition.datatype in (LMS_DataType.STRING, LMS_DataType.
|
|
89
|
+
if definition.datatype in (LMS_DataType.STRING, LMS_DataType.ENUM):
|
|
79
90
|
fields[definition.name] = LMS_Field(data[definition.name], definition)
|
|
80
91
|
continue
|
|
81
92
|
|
|
82
93
|
value = data[definition.name]
|
|
83
94
|
match definition.datatype:
|
|
84
|
-
case LMS_DataType.BYTES:
|
|
85
|
-
casted_value = bytes.fromhex(value)
|
|
86
95
|
case LMS_DataType.BOOL:
|
|
87
96
|
if value not in ("false", "true"):
|
|
88
97
|
raise ValueError("Invalid boolean value!")
|
|
@@ -103,9 +112,9 @@ class LMS_Field:
|
|
|
103
112
|
"""
|
|
104
113
|
|
|
105
114
|
def __init__(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
115
|
+
self,
|
|
116
|
+
value: int | str | float | bytes | bool,
|
|
117
|
+
definition: FieldDefinition,
|
|
109
118
|
):
|
|
110
119
|
_verify_value_from_definition(value, definition)
|
|
111
120
|
self._definition = definition
|
|
@@ -113,7 +122,7 @@ class LMS_Field:
|
|
|
113
122
|
|
|
114
123
|
def __repr__(self):
|
|
115
124
|
if self.datatype is LMS_DataType.LIST:
|
|
116
|
-
return f"LMS_Field(value={self._value!r}, list_items={self.
|
|
125
|
+
return f"LMS_Field(value={self._value!r}, list_items={self.enum_members!r})"
|
|
117
126
|
|
|
118
127
|
return f"LMS_Field(value={self._value!r}, type={self.datatype!r})"
|
|
119
128
|
|
|
@@ -138,9 +147,9 @@ class LMS_Field:
|
|
|
138
147
|
return self._definition.datatype
|
|
139
148
|
|
|
140
149
|
@property
|
|
141
|
-
def
|
|
150
|
+
def enum_members(self) -> list[str]:
|
|
142
151
|
"""The list items bound to the field instance. Only is valid for ``LMS_Datatype.LIST`` values."""
|
|
143
|
-
return self._definition.
|
|
152
|
+
return self._definition.enum_members
|
|
144
153
|
|
|
145
154
|
@value.setter
|
|
146
155
|
def value(self, new_value: int | str | float | bytes | bool):
|
|
@@ -149,7 +158,7 @@ class LMS_Field:
|
|
|
149
158
|
|
|
150
159
|
|
|
151
160
|
def _verify_value_from_definition(
|
|
152
|
-
|
|
161
|
+
value: int | str | float | bytes | bool, definition: FieldDefinition
|
|
153
162
|
) -> None:
|
|
154
163
|
datatype = definition.datatype
|
|
155
164
|
|
|
@@ -157,16 +166,11 @@ def _verify_value_from_definition(
|
|
|
157
166
|
return
|
|
158
167
|
|
|
159
168
|
match datatype:
|
|
160
|
-
case LMS_DataType.
|
|
161
|
-
if
|
|
162
|
-
raise ValueError("Byte types only work for values of length 1!")
|
|
163
|
-
else:
|
|
164
|
-
return
|
|
165
|
-
case LMS_DataType.LIST if isinstance(value, str):
|
|
166
|
-
if value not in definition.list_items:
|
|
169
|
+
case LMS_DataType.ENUM if isinstance(value, str):
|
|
170
|
+
if value not in definition.enum_members.values():
|
|
167
171
|
raise ValueError(
|
|
168
172
|
f"""The value of '{value}' provided for field '{definition.name}' is not a
|
|
169
|
-
valid item in the list {definition.
|
|
173
|
+
valid item in the list {definition.enum_members}."""
|
|
170
174
|
)
|
|
171
175
|
else:
|
|
172
176
|
return
|
lms/common/lms_constants.py
CHANGED
lms/common/stream/section.py
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
|
-
from typing import Any, Callable, Generator
|
|
1
|
+
from typing import Any, Callable, Generator, Sequence
|
|
2
2
|
|
|
3
|
+
from lms.common.lms_constants import SECTION_ALIGNMENT, SECTION_PADDING_BYTE
|
|
3
4
|
from lms.fileio.io import FileReader, FileWriter
|
|
4
5
|
|
|
5
|
-
SECTION_PADDING_BYTE = b"\xab"
|
|
6
|
-
SECTION_ALIGNMENT = 16
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
def read_section_data(
|
|
10
|
-
reader: FileReader, section_count: int
|
|
11
|
-
) -> Generator[tuple[str, int], Any, None]:
|
|
7
|
+
def read_section_data(reader: FileReader, section_count: int) -> Generator[tuple[str, int], Any, None]:
|
|
12
8
|
reader.seek(0x20)
|
|
13
9
|
for _ in range(section_count):
|
|
14
10
|
magic = reader.read_string_len(4)
|
|
@@ -20,15 +16,15 @@ def read_section_data(
|
|
|
20
16
|
yield magic, size
|
|
21
17
|
|
|
22
18
|
reader.seek(end)
|
|
23
|
-
reader.align(
|
|
19
|
+
reader.align(SECTION_ALIGNMENT)
|
|
24
20
|
|
|
25
21
|
|
|
26
22
|
def write_section(
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
23
|
+
writer: FileWriter,
|
|
24
|
+
magic: str,
|
|
25
|
+
section_call: Callable,
|
|
26
|
+
data: Sequence[Any],
|
|
27
|
+
*write_arguments: Any,
|
|
32
28
|
) -> None:
|
|
33
29
|
writer.write_string(magic)
|
|
34
30
|
size_offset = writer.tell()
|
lms/fileio/io.py
CHANGED
|
@@ -1,47 +1,54 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import struct
|
|
4
|
-
from
|
|
4
|
+
from enum import StrEnum
|
|
5
|
+
from typing import BinaryIO, Generator, Type
|
|
5
6
|
|
|
6
|
-
from io import BytesIO, IOBase
|
|
7
|
+
from io import BytesIO, IOBase, TextIOBase
|
|
7
8
|
from lms.fileio.encoding import FileEncoding
|
|
8
9
|
|
|
9
10
|
PADDING_BYTE = b"\x00"
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
12
|
+
|
|
13
|
+
class StructLETypes(StrEnum):
|
|
14
|
+
UINT8 = "<B"
|
|
15
|
+
UINT16 = "<H"
|
|
16
|
+
UINT32 = "<I"
|
|
17
|
+
INT8 = "<b"
|
|
18
|
+
INT16 = "<h"
|
|
19
|
+
INT32 = "<i"
|
|
20
|
+
FLOAT32 = "<f"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class StructBETypes(StrEnum):
|
|
24
|
+
UINT8 = ">B"
|
|
25
|
+
UINT16 = ">H"
|
|
26
|
+
UINT32 = ">I"
|
|
27
|
+
INT8 = ">b"
|
|
28
|
+
INT16 = ">h"
|
|
29
|
+
INT32 = ">i"
|
|
30
|
+
FLOAT32 = ">f"
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
class FileReader:
|
|
34
|
-
def __init__(self, data: BinaryIO | bytes, big_endian: bool = False):
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
def __init__(self, data: BinaryIO | bytes | bytearray | memoryview, big_endian: bool = False):
|
|
35
|
+
self._stream: BinaryIO = None
|
|
36
|
+
|
|
37
|
+
match data:
|
|
38
|
+
case IOBase() if not isinstance(data, TextIOBase):
|
|
39
|
+
self._stream = data
|
|
40
|
+
case bytes() | bytearray() | memoryview():
|
|
41
|
+
self._stream = BytesIO(data)
|
|
42
|
+
case _:
|
|
43
|
+
raise TypeError("The stream provided is not valid!")
|
|
41
44
|
|
|
42
45
|
self.encoding = FileEncoding.UTF8
|
|
43
46
|
self.is_big_endian = big_endian
|
|
44
47
|
|
|
48
|
+
@property
|
|
49
|
+
def datatypes(self) -> Type[StructLETypes | StructBETypes]:
|
|
50
|
+
return StructLETypes if not self.is_big_endian else StructBETypes
|
|
51
|
+
|
|
45
52
|
def tell(self) -> int:
|
|
46
53
|
return self._stream.tell()
|
|
47
54
|
|
|
@@ -49,17 +56,13 @@ class FileReader:
|
|
|
49
56
|
self._stream.read(length)
|
|
50
57
|
|
|
51
58
|
def align(self, alignment: int) -> None:
|
|
52
|
-
|
|
53
|
-
self.skip(alignment)
|
|
59
|
+
self.skip((-self.tell() % alignment + alignment) % alignment)
|
|
54
60
|
|
|
55
61
|
def read_bytes(self, length: int) -> bytes:
|
|
56
62
|
return self._stream.read(length)
|
|
57
63
|
|
|
58
64
|
def seek(self, offset: int, whence: int = 0) -> None:
|
|
59
|
-
if offset < 0
|
|
60
|
-
self._stream.seek(self.tell() + offset)
|
|
61
|
-
else:
|
|
62
|
-
self._stream.seek(offset, whence)
|
|
65
|
+
self._stream.seek(self.tell() + offset) if offset < 0 else self._stream.seek(offset, whence)
|
|
63
66
|
|
|
64
67
|
def read_offset_array(self, count: int) -> Generator[int, None, None]:
|
|
65
68
|
start = self.tell() - 4
|
|
@@ -72,25 +75,25 @@ class FileReader:
|
|
|
72
75
|
return [self.read_uint16() for _ in range(count)]
|
|
73
76
|
|
|
74
77
|
def read_int8(self) -> int:
|
|
75
|
-
return struct.unpack(self.
|
|
78
|
+
return struct.unpack(self.datatypes.INT8, self._stream.read(1))[0]
|
|
76
79
|
|
|
77
80
|
def read_int16(self) -> int:
|
|
78
|
-
return struct.unpack(self.
|
|
81
|
+
return struct.unpack(self.datatypes.INT16, self._stream.read(2))[0]
|
|
79
82
|
|
|
80
83
|
def read_int32(self) -> int:
|
|
81
|
-
return struct.unpack(self.
|
|
84
|
+
return struct.unpack(self.datatypes.INT32, self._stream.read(4))[0]
|
|
82
85
|
|
|
83
86
|
def read_uint8(self) -> int:
|
|
84
|
-
return struct.unpack(self.
|
|
87
|
+
return struct.unpack(self.datatypes.UINT8, self._stream.read(1))[0]
|
|
85
88
|
|
|
86
89
|
def read_uint16(self) -> int:
|
|
87
|
-
return struct.unpack(self.
|
|
90
|
+
return struct.unpack(self.datatypes.UINT16, self._stream.read(2))[0]
|
|
88
91
|
|
|
89
92
|
def read_uint32(self) -> int:
|
|
90
|
-
return struct.unpack(self.
|
|
93
|
+
return struct.unpack(self.datatypes.UINT32, self._stream.read(4))[0]
|
|
91
94
|
|
|
92
95
|
def read_float32(self) -> float:
|
|
93
|
-
return struct.unpack(self.
|
|
96
|
+
return struct.unpack(self.datatypes.FLOAT32, self._stream.read(4))[0]
|
|
94
97
|
|
|
95
98
|
def read_string_offset(self, section_start: int) -> str:
|
|
96
99
|
offset = self.read_uint32()
|
|
@@ -104,12 +107,10 @@ class FileReader:
|
|
|
104
107
|
return self._stream.read(length).decode("UTF-8")
|
|
105
108
|
|
|
106
109
|
def read_encoded_string(self):
|
|
107
|
-
|
|
108
|
-
while (
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
message += raw_char
|
|
112
|
-
return message.decode(self.encoding.to_string_format(self.is_big_endian))
|
|
110
|
+
string = b""
|
|
111
|
+
while (raw_char := self.read_bytes(self.encoding.width)) != self.encoding.terminator:
|
|
112
|
+
string += raw_char
|
|
113
|
+
return string.decode(self.encoding.to_string_format(self.is_big_endian))
|
|
113
114
|
|
|
114
115
|
def read_len_string_encoded(self):
|
|
115
116
|
self.align(self.encoding.width)
|
|
@@ -118,9 +119,6 @@ class FileReader:
|
|
|
118
119
|
self.encoding.to_string_format(self.is_big_endian)
|
|
119
120
|
)
|
|
120
121
|
|
|
121
|
-
def _get_datatype(self, name: str) -> str:
|
|
122
|
-
return STRUCT_TYPES["little" if not self.is_big_endian else "big"][name]
|
|
123
|
-
|
|
124
122
|
|
|
125
123
|
class FileWriter:
|
|
126
124
|
def __init__(self, encoding: FileEncoding):
|
|
@@ -128,6 +126,10 @@ class FileWriter:
|
|
|
128
126
|
self.encoding = encoding
|
|
129
127
|
self.is_big_endian = False
|
|
130
128
|
|
|
129
|
+
@property
|
|
130
|
+
def datatypes(self) -> Type[StructLETypes | StructBETypes]:
|
|
131
|
+
return StructLETypes if not self.is_big_endian else StructBETypes
|
|
132
|
+
|
|
131
133
|
def skip(self, length: int) -> None:
|
|
132
134
|
self.data.seek(length, 1)
|
|
133
135
|
|
|
@@ -155,34 +157,34 @@ class FileWriter:
|
|
|
155
157
|
self.write_uint16(number)
|
|
156
158
|
|
|
157
159
|
def write_int8(self, value: int) -> None:
|
|
158
|
-
self.data.write(struct.pack(self.
|
|
160
|
+
self.data.write(struct.pack(self.datatypes.INT8, value))
|
|
159
161
|
|
|
160
162
|
def write_int16(self, value: int) -> None:
|
|
161
|
-
self.data.write(struct.pack(self.
|
|
163
|
+
self.data.write(struct.pack(self.datatypes.INT16, value))
|
|
162
164
|
|
|
163
165
|
def write_int32(self, value: int) -> None:
|
|
164
|
-
self.data.write(struct.pack(self.
|
|
166
|
+
self.data.write(struct.pack(self.datatypes.INT32, value))
|
|
165
167
|
|
|
166
168
|
def write_uint8(self, value: int) -> None:
|
|
167
|
-
self.data.write(struct.pack(self.
|
|
169
|
+
self.data.write(struct.pack(self.datatypes.UINT8, value))
|
|
168
170
|
|
|
169
171
|
def write_uint16(self, value: int) -> None:
|
|
170
|
-
self.data.write(struct.pack(self.
|
|
172
|
+
self.data.write(struct.pack(self.datatypes.UINT16, value))
|
|
171
173
|
|
|
172
174
|
def write_uint32(self, value: int) -> None:
|
|
173
|
-
self.data.write(struct.pack(self.
|
|
175
|
+
self.data.write(struct.pack(self.datatypes.UINT32, value))
|
|
174
176
|
|
|
175
177
|
def write_float32(self, value: float) -> None:
|
|
176
|
-
self.data.write(struct.pack(self.
|
|
178
|
+
self.data.write(struct.pack(self.datatypes.FLOAT32, value))
|
|
177
179
|
|
|
178
|
-
def write_string(self, string: str):
|
|
180
|
+
def write_string(self, string: str) -> None:
|
|
179
181
|
self.write_bytes(string.encode("UTF-8"))
|
|
180
182
|
|
|
181
183
|
def write_len_encoded_string(self, string: str) -> None:
|
|
182
184
|
self.write_uint16(len(string) * self.encoding.width)
|
|
183
185
|
self.write_encoded_string(string, False)
|
|
184
186
|
|
|
185
|
-
def write_encoded_string(self, string: str, terminate: bool = True):
|
|
187
|
+
def write_encoded_string(self, string: str, terminate: bool = True) -> None:
|
|
186
188
|
self.write_bytes(
|
|
187
189
|
string.encode(self.encoding.to_string_format(self.is_big_endian))
|
|
188
190
|
)
|
|
@@ -192,8 +194,5 @@ class FileWriter:
|
|
|
192
194
|
def _align(self, number: int, alignment: int) -> int:
|
|
193
195
|
return (-number % alignment + alignment) % alignment
|
|
194
196
|
|
|
195
|
-
def _get_datatype(self, name: str) -> str:
|
|
196
|
-
return STRUCT_TYPES["little" if not self.is_big_endian else "big"][name]
|
|
197
|
-
|
|
198
197
|
def get_data(self) -> bytes:
|
|
199
198
|
return self.data.getvalue()
|