PyLibMS 2.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.
Files changed (57) hide show
  1. LMS/Common/LMS_Exceptions.py +16 -0
  2. LMS/Common/LMS_FileInfo.py +11 -0
  3. LMS/Common/Stream/FileInfo.py +64 -0
  4. LMS/Common/Stream/Hashtable.py +87 -0
  5. LMS/Common/Stream/Section.py +60 -0
  6. LMS/Common/__init__.py +0 -0
  7. LMS/Config/Definitions/Attributes.py +14 -0
  8. LMS/Config/Definitions/Tags.py +52 -0
  9. LMS/Config/Definitions/Value.py +11 -0
  10. LMS/Config/Definitions/__init__.py +0 -0
  11. LMS/Config/TitleConfig.py +208 -0
  12. LMS/Config/__init__.py +0 -0
  13. LMS/Field/LMS_DataType.py +83 -0
  14. LMS/Field/LMS_Field.py +123 -0
  15. LMS/Field/Stream.py +56 -0
  16. LMS/Field/__init__.py +0 -0
  17. LMS/FileIO/Encoding.py +42 -0
  18. LMS/FileIO/Stream.py +181 -0
  19. LMS/Message/Definitions/LMS_FieldMap.py +5 -0
  20. LMS/Message/Definitions/LMS_MessageText.py +124 -0
  21. LMS/Message/Definitions/__init__.py +0 -0
  22. LMS/Message/MSBT.py +63 -0
  23. LMS/Message/MSBTEntry.py +33 -0
  24. LMS/Message/MSBTStream.py +138 -0
  25. LMS/Message/Section/ATR1.py +101 -0
  26. LMS/Message/Section/TSY1.py +13 -0
  27. LMS/Message/Section/TXT2.py +79 -0
  28. LMS/Message/Section/__init__.py +0 -0
  29. LMS/Message/Tag/LMS_Tag.py +169 -0
  30. LMS/Message/Tag/LMS_TagExceptions.py +7 -0
  31. LMS/Message/Tag/Stream.py +149 -0
  32. LMS/Message/Tag/System.yaml +47 -0
  33. LMS/Message/Tag/Tag_Formats.py +10 -0
  34. LMS/Message/Tag/__init__.py +0 -0
  35. LMS/Message/__init__.py +0 -0
  36. LMS/Project/Definitions/Attribute.py +27 -0
  37. LMS/Project/Definitions/Color.py +10 -0
  38. LMS/Project/Definitions/Style.py +14 -0
  39. LMS/Project/Definitions/Tag.py +58 -0
  40. LMS/Project/Definitions/__init__.py +0 -0
  41. LMS/Project/MSBP.py +64 -0
  42. LMS/Project/MSBPRead.py +101 -0
  43. LMS/Project/Section/ALI2.py +19 -0
  44. LMS/Project/Section/ATI2.py +17 -0
  45. LMS/Project/Section/CLR1.py +15 -0
  46. LMS/Project/Section/SYL3.py +17 -0
  47. LMS/Project/Section/String.py +18 -0
  48. LMS/Project/Section/TAG2.py +19 -0
  49. LMS/Project/Section/TGG2.py +20 -0
  50. LMS/Project/Section/TGP2.py +26 -0
  51. LMS/Project/__init__.py +0 -0
  52. LMS/__init__.py +0 -0
  53. pylibms-2.0.0.dist-info/METADATA +40 -0
  54. pylibms-2.0.0.dist-info/RECORD +57 -0
  55. pylibms-2.0.0.dist-info/WHEEL +5 -0
  56. pylibms-2.0.0.dist-info/licenses/LICENSE +7 -0
  57. pylibms-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,138 @@
1
+ from typing import BinaryIO
2
+
3
+ from LMS.Common import LMS_Exceptions
4
+ from LMS.Common.Stream.FileInfo import read_file_info, write_file_info
5
+ from LMS.Common.Stream.Hashtable import read_labels, write_labels
6
+ from LMS.Common.Stream.Section import (read_section_data, write_section,
7
+ write_unsupported_section)
8
+ from LMS.Config.TitleConfig import AttributeConfig, TagConfig
9
+ from LMS.FileIO.Stream import FileReader, FileWriter
10
+ from LMS.Message.MSBT import MSBT
11
+ from LMS.Message.MSBTEntry import MSBTEntry
12
+ from LMS.Message.Section.ATR1 import (read_decoded_atr1, read_encoded_atr1,
13
+ write_decoded_atr1, write_encoded_atr1)
14
+ from LMS.Message.Section.TSY1 import read_tsy1, write_tsy1
15
+ from LMS.Message.Section.TXT2 import read_txt2, write_txt2
16
+
17
+
18
+ def read_msbt(
19
+ stream: BinaryIO,
20
+ attribute_config: AttributeConfig = None,
21
+ tag_config: TagConfig = None,
22
+ ) -> MSBT:
23
+ """Reads a MSBT file from a stream.
24
+
25
+ :param stream: a stream object.
26
+
27
+ ## Usage
28
+ ```
29
+ with open(file_path, "rb") as file:
30
+ msbt = read_msbt(file, "Game.yaml")
31
+ ...
32
+ ```"""
33
+ if stream is None:
34
+ raise ValueError("Stream must be valid!")
35
+
36
+ reader = FileReader(stream)
37
+ file_info = read_file_info(reader, "MsgStdBn")
38
+
39
+ file = MSBT(file_info)
40
+
41
+ if attribute_config is not None:
42
+ file.encoded_attributes = False
43
+
44
+ messages = attributes = style_indexes = None
45
+ for magic, size in read_section_data(reader, file_info.section_count):
46
+ match magic:
47
+ case "LBL1":
48
+ labels, slot_count = read_labels(reader)
49
+ file.slot_count = slot_count
50
+ case "ATR1":
51
+ # Map to shared variable to make assignment cleaner
52
+ if attribute_config is None:
53
+ data = read_encoded_atr1(reader, size)
54
+ else:
55
+ file.encoded_attributes = False
56
+ data = read_decoded_atr1(reader, attribute_config)
57
+
58
+ attributes, size_per_attribute, string_table = data
59
+ file.size_per_attribute = size_per_attribute
60
+ file.attr_string_table = string_table
61
+ case "TXT2":
62
+ messages = read_txt2(reader, file_info.encoding, tag_config)
63
+ case "TSY1":
64
+ style_indexes = read_tsy1(reader, len(labels))
65
+ case _:
66
+ file.unsupported_sections[magic] = reader.read_bytes(size)
67
+
68
+ file.section_list.append(magic)
69
+
70
+ for i, label in labels.items():
71
+ text = None if messages is None else messages[i]
72
+ attribute = None if attributes is None else attributes[i]
73
+ style_index = None if style_indexes is None else style_indexes[i]
74
+ file.entries.append(MSBTEntry(label, text, attribute, style_index))
75
+
76
+ return file
77
+
78
+
79
+ def write_msbt(stream: BinaryIO, file: MSBT) -> None:
80
+ """Writes a MSBT file to a stream.
81
+
82
+ :param stream: the filestream.
83
+ :param file: the MSBT file object.
84
+
85
+ ## Usage
86
+ ```
87
+ with open(file_path, "wb") as file:
88
+ write_msbt(file, msbt)
89
+ ...
90
+ ```
91
+ """
92
+ if stream is None:
93
+ raise LMS_Exceptions.LMS_Error("Stream is not valid!")
94
+
95
+ if not isinstance(file, MSBT):
96
+ raise LMS_Exceptions.LMS_Error("File provided is not a MSBT.")
97
+
98
+ writer = FileWriter(file.info.encoding)
99
+ write_file_info(writer, "MsgStdBn", file.info)
100
+
101
+ for section in file.section_list:
102
+ match section:
103
+ case "LBL1":
104
+ labels = [entry.name for entry in file.entries]
105
+ write_section(writer, "LBL1", write_labels, labels, file.slot_count)
106
+ case "ATR1":
107
+ attributes = [entry.attribute for entry in file.entries]
108
+ if file.encoded_attributes:
109
+ write_section(
110
+ writer,
111
+ "ATR1",
112
+ write_encoded_atr1,
113
+ attributes,
114
+ file.size_per_attribute,
115
+ file.attr_string_table,
116
+ )
117
+ else:
118
+ write_section(
119
+ writer,
120
+ "ATR1",
121
+ write_decoded_atr1,
122
+ attributes,
123
+ file.size_per_attribute,
124
+ )
125
+ case "TXT2":
126
+ messages = [entry.message for entry in file.entries]
127
+ write_section(writer, "TXT2", write_txt2, messages)
128
+ case "TSY1":
129
+ style_indexes = [entry.style_index for entry in file.entries]
130
+ write_section(writer, "TSY1", write_tsy1, style_indexes)
131
+ case _:
132
+ write_unsupported_section(
133
+ writer, section, file.unsupported_sections[section]
134
+ )
135
+
136
+ writer.seek(0x12)
137
+ writer.write_uint32(writer.get_stream_size())
138
+ stream.write(writer.get_data())
@@ -0,0 +1,101 @@
1
+ from LMS.Config.Definitions.Attributes import AttributeConfig
2
+ from LMS.Field.LMS_DataType import LMS_DataType
3
+ from LMS.Field.LMS_Field import LMS_DataType, LMS_Field
4
+ from LMS.Field.Stream import read_field, write_field
5
+ from LMS.FileIO.Stream import FileReader, FileWriter
6
+ from LMS.Message.Definitions.LMS_FieldMap import LMS_FieldMap
7
+
8
+
9
+ def read_encoded_atr1(
10
+ reader: FileReader, section_size: int
11
+ ) -> tuple[list[bytes], int, list[str]]:
12
+ absolute_size = section_size + reader.tell()
13
+
14
+ attribute_count = reader.read_uint32()
15
+ size_per_attribute = reader.read_uint32()
16
+
17
+ attributes = [reader.read_bytes(size_per_attribute) for _ in range(attribute_count)]
18
+ string_table = None
19
+
20
+ if section_size > 8 + size_per_attribute * attribute_count:
21
+ string_table = reader.read_bytes(absolute_size - reader.tell())
22
+
23
+ return (attributes, size_per_attribute, string_table)
24
+
25
+
26
+ def write_encoded_atr1(
27
+ writer: FileWriter,
28
+ attributes: list[bytes],
29
+ size_per_attribute: int,
30
+ string_table: bytes | None,
31
+ ):
32
+ writer.write_uint32(len(attributes))
33
+
34
+ if attributes:
35
+ writer.write_uint32(size_per_attribute)
36
+ else:
37
+ writer.write_uint32(0)
38
+
39
+ for attr in attributes:
40
+ writer.write_bytes(attr)
41
+
42
+ if string_table is not None:
43
+ writer.write_bytes(string_table)
44
+
45
+
46
+ def read_decoded_atr1(
47
+ reader: FileReader, structure: AttributeConfig
48
+ ) -> tuple[list[LMS_FieldMap], int, None]:
49
+ section_start = reader.tell()
50
+
51
+ attr_count = reader.read_uint32()
52
+ size_per_attr = reader.read_uint32()
53
+
54
+ attributes = []
55
+ attr_start = reader.tell()
56
+
57
+ for _ in range(attr_count):
58
+ reader.seek(attr_start)
59
+
60
+ attribute = {}
61
+ for definition in structure.definitions:
62
+ if definition.datatype is LMS_DataType.STRING:
63
+ last = reader.tell() + 4
64
+ reader.seek(section_start + reader.read_uint32())
65
+ value = LMS_Field(reader.read_str_variable_encoding(), definition)
66
+
67
+ reader.seek(last)
68
+ else:
69
+ value = read_field(reader, definition)
70
+
71
+ attribute[definition.name] = value
72
+
73
+ attributes.append(attribute)
74
+
75
+ # Move to the start of the next attribute
76
+ attr_start += size_per_attr
77
+
78
+ return attributes, size_per_attr, None
79
+
80
+
81
+ def write_decoded_atr1(
82
+ writer: FileWriter, attributes: list[LMS_FieldMap], size_per_attribute: int
83
+ ) -> None:
84
+ writer.write_uint32(len(attributes))
85
+ writer.write_uint32(size_per_attribute)
86
+
87
+ string_table = []
88
+ string_offset = 8 + size_per_attribute * len(attributes)
89
+ for attr in attributes:
90
+ for field in attr.values():
91
+ if field.datatype is not LMS_DataType.STRING:
92
+ write_field(writer, field)
93
+ else:
94
+ writer.write_uint32(string_offset)
95
+ string_offset += (
96
+ len(field.value) * writer.encoding.width
97
+ ) + writer.encoding.width
98
+ string_table.append(field.value)
99
+
100
+ for string in string_table:
101
+ writer.write_variable_encoding_string(string)
@@ -0,0 +1,13 @@
1
+ from LMS.FileIO.Stream import FileReader, FileWriter
2
+
3
+
4
+ def read_tsy1(reader: FileReader, message_count: int) -> list[int]:
5
+ style_indexes = []
6
+ for _ in range(message_count):
7
+ style_indexes.append(reader.read_uint32())
8
+ return style_indexes
9
+
10
+
11
+ def write_tsy1(writer: FileWriter, style_indexes: list[int]) -> None:
12
+ for i in style_indexes:
13
+ writer.write_uint32(i)
@@ -0,0 +1,79 @@
1
+ from LMS.Config.Definitions.Tags import TagConfig
2
+ from LMS.FileIO.Encoding import FileEncoding
3
+ from LMS.FileIO.Stream import FileReader, FileWriter
4
+ from LMS.Message.Definitions.LMS_MessageText import LMS_MessageText
5
+ from LMS.Message.Tag.LMS_Tag import LMS_TagBase
6
+ from LMS.Message.Tag.Stream import read_tag, write_tag
7
+
8
+
9
+ def read_txt2(
10
+ reader: FileReader, encoding: FileEncoding, config: TagConfig
11
+ ) -> list[LMS_MessageText]:
12
+ text_list = []
13
+ count = reader.read_uint32()
14
+
15
+ encoding_format = encoding.to_string_format(reader.big_endian)
16
+ tag_indicator = b"\x0e" + (b"\x00" * (reader.encoding.width - 1))
17
+
18
+ if reader.big_endian:
19
+ tag_indicator = tag_indicator[::-1]
20
+
21
+ for offset in reader.read_offset_array(count):
22
+ reader.seek(offset)
23
+
24
+ text, parts = b"", []
25
+ while (data := reader.read_bytes(encoding.width)) != encoding.terminator:
26
+ if data == tag_indicator:
27
+ # Add all the text before the control tag
28
+ parts.append(text.decode(encoding_format))
29
+
30
+ # Read the tag data
31
+ group_index = reader.read_uint16()
32
+ tag_index = reader.read_uint16()
33
+ param_size = reader.read_uint16()
34
+
35
+ tag = read_tag(reader, param_size, group_index, tag_index, config)
36
+
37
+ parts.append(tag)
38
+ text = b""
39
+ # TODO: Add support for closing tags
40
+ elif data == b"\x0f":
41
+ raise NotImplementedError(
42
+ "MSBT files with closing tags are unsupported at this time!"
43
+ )
44
+ else:
45
+ text += data
46
+
47
+ # Add the remaining text in case there were no control tags
48
+ if text:
49
+ parts.append(text.decode(encoding_format))
50
+
51
+ message = LMS_MessageText(parts, config)
52
+ text_list.append(message)
53
+
54
+ return text_list
55
+
56
+
57
+ def write_txt2(writer: FileWriter, messages: list[LMS_MessageText]) -> None:
58
+ start = writer.tell()
59
+ writer.write_uint32(len(messages))
60
+
61
+ offset = 4 + 4 * len(messages)
62
+ for message in messages:
63
+ writer.write_uint32(offset)
64
+ next = writer.tell()
65
+
66
+ writer.seek(start + offset)
67
+ text_start = writer.tell()
68
+ for part in message:
69
+ if isinstance(part, LMS_TagBase):
70
+ write_tag(writer, part)
71
+ else:
72
+ writer.write_variable_encoding_string(part, False)
73
+
74
+ writer.write_bytes(writer.encoding.terminator)
75
+ offset += writer.tell() - text_start
76
+ writer.seek(next)
77
+
78
+ # Seek to the end of the text to ensure end data can be written properly
79
+ writer.seek(offset + start)
File without changes
@@ -0,0 +1,169 @@
1
+ import re
2
+ from abc import ABC, abstractmethod
3
+
4
+ from LMS.Config.Definitions.Tags import TagConfig
5
+ from LMS.Field.LMS_Field import LMS_Field, cast_value
6
+ from LMS.Message.Definitions.LMS_FieldMap import LMS_FieldMap
7
+ from LMS.Message.Tag.LMS_TagExceptions import LMS_InvalidTagFormatError
8
+ from LMS.Message.Tag.Tag_Formats import (DECODED_FORMAT, ENCODED_FORMAT,
9
+ PARAMETER_FORMAT)
10
+
11
+
12
+ class LMS_TagBase(ABC):
13
+ def __init__(
14
+ self,
15
+ group_index: int,
16
+ tag_index: int,
17
+ parameters: LMS_FieldMap | list[str] | None = None,
18
+ group_name: str = None,
19
+ tag_name: str = None,
20
+ ):
21
+ self._group_index = group_index
22
+ self._tag_index = tag_index
23
+ self._parameters = parameters or {}
24
+ self._group_name, self._tag_name = group_name, tag_name
25
+
26
+ @property
27
+ def group_index(self) -> int:
28
+ """The index of the group."""
29
+ return self._group_index
30
+
31
+ @property
32
+ def tag_index(self) -> int:
33
+ """The index of the tag in it's group."""
34
+ return self._tag_index
35
+
36
+ @property
37
+ def group_name(self) -> str:
38
+ """The name of the tag group."""
39
+ return self._group_name
40
+
41
+ @property
42
+ def tag_name(self) -> str:
43
+ """The name of the tag in the tag group."""
44
+ return self._tag_name
45
+
46
+ @property
47
+ def parameters(self) -> list[str] | LMS_FieldMap:
48
+ """The parameters of the tag."""
49
+ return self._parameters
50
+
51
+ @abstractmethod
52
+ def to_text(self) -> str:
53
+ """Converts the tag to its string representation."""
54
+ pass
55
+
56
+ @classmethod
57
+ @abstractmethod
58
+ def from_string(self, string: str, config: TagConfig | None = None) -> str:
59
+ pass
60
+
61
+
62
+ class LMS_EncodedTag(LMS_TagBase):
63
+ """A class that represents an encoded tag.
64
+
65
+ Example encoded tags:
66
+ - `[0:3 00-00-00-FF]`
67
+ - `[0:4]`
68
+ - `[1:0 01-00-00-CD]`"""
69
+
70
+ def __init__(
71
+ self,
72
+ group_index: int,
73
+ tag_index: int,
74
+ parameters: list[str] = None,
75
+ group_name: str = None,
76
+ tag_name: str = None,
77
+ ):
78
+ super().__init__(group_index, tag_index, parameters)
79
+ self._group_name = group_name
80
+ self._tag_name = tag_name
81
+
82
+ def to_text(self) -> str:
83
+ if self._group_name is not None and self._tag_name is not None:
84
+ # Determine what to display based on if the names are provided
85
+ group = self._group_name or self._group_index
86
+ tag = self._tag_name or self._tag_index
87
+ else:
88
+ group, tag = self._group_index, self._tag_index
89
+
90
+ if not self._parameters:
91
+ return f"[{group}:{tag}]"
92
+
93
+ parameters = "-".join(self.parameters)
94
+ return f"[{group}:{tag} {parameters}]"
95
+
96
+ @classmethod
97
+ def from_string(cls, string: str, config: TagConfig | None = None):
98
+ if match := re.match(ENCODED_FORMAT, string):
99
+ group, tag = match.group(1), match.group(2)
100
+ parameters = match.group(3).split("-")
101
+
102
+ if group.isdigit() and tag.isdigit():
103
+ return cls(int(group), int(tag), parameters)
104
+
105
+ tag_definition = config.get_definition_by_names(group, tag)
106
+ return cls(
107
+ group,
108
+ tag,
109
+ parameters,
110
+ tag_definition.group_name,
111
+ tag_definition.tag_name,
112
+ )
113
+ else:
114
+ raise LMS_InvalidTagFormatError(
115
+ f"Invalid encoded tag format detected for tag'{string}'"
116
+ )
117
+
118
+
119
+ class LMS_DecodedTag(LMS_TagBase):
120
+ """A class that represents a decoded tag.
121
+
122
+ Example encoded tags:
123
+ - `[System:Color r="0" g="255" b="255" a="255"]`
124
+ - `[System:Pagebreak]`
125
+ - `[Mii:Nickname buffer="1" type="Text" conversion="None"]`"""
126
+
127
+ def __init__(
128
+ self,
129
+ group_index: int,
130
+ tag_index: int,
131
+ group_name: str,
132
+ tag_name: str,
133
+ parameters: LMS_FieldMap = None,
134
+ ):
135
+ super().__init__(group_index, tag_index, parameters, group_name, tag_name)
136
+
137
+ def to_text(self) -> str:
138
+ if not self._parameters:
139
+ return f"[{self.group_name}:{self.tag_name}]"
140
+
141
+ parameters = " ".join(
142
+ f'{key}="{param._value}"' for key, param in self._parameters.items()
143
+ )
144
+ return f"[{self.group_name}:{self.tag_name} {parameters}]"
145
+
146
+ @classmethod
147
+ def from_string(cls, tag: str, config: TagConfig):
148
+ if match := re.match(DECODED_FORMAT, tag):
149
+ group_name, tag_name = match.group(1), match.group(2)
150
+ parameters = dict(re.findall(PARAMETER_FORMAT, tag))
151
+
152
+ tag_definition = config.get_definition_by_names(group_name, tag_name)
153
+ for definition in tag_definition.parameters:
154
+ casted_value = cast_value(
155
+ parameters[definition.name], definition.datatype
156
+ )
157
+ parameters[definition.name] = LMS_Field(casted_value, definition)
158
+
159
+ return cls(
160
+ tag_definition.group_index,
161
+ tag_definition.tag_index,
162
+ group_name,
163
+ tag_name,
164
+ parameters,
165
+ )
166
+ else:
167
+ raise LMS_InvalidTagFormatError(
168
+ f"Invalid decoded tag format detectefd for tag '{tag}'"
169
+ )
@@ -0,0 +1,7 @@
1
+ class LMS_TagReadingError(Exception): ...
2
+
3
+
4
+ class LMS_TagWritingException(Exception): ...
5
+
6
+
7
+ class LMS_InvalidTagFormatError(Exception): ...
@@ -0,0 +1,149 @@
1
+ from LMS.Config.Definitions.Tags import TagConfig, TagDefinition
2
+ from LMS.Field.LMS_DataType import LMS_DataType
3
+ from LMS.Field.LMS_Field import LMS_Field
4
+ from LMS.Field.Stream import read_field, write_field
5
+ from LMS.FileIO.Stream import FileReader, FileWriter
6
+ from LMS.Message.Definitions.LMS_FieldMap import LMS_FieldMap
7
+ from LMS.Message.Tag.LMS_Tag import LMS_DecodedTag, LMS_EncodedTag, LMS_TagBase
8
+ from LMS.Message.Tag.LMS_TagExceptions import (LMS_TagReadingError,
9
+ LMS_TagWritingException)
10
+
11
+
12
+ def read_tag(
13
+ reader: FileReader,
14
+ param_size: int,
15
+ group_index: int,
16
+ tag_index: int,
17
+ config: TagConfig | None,
18
+ ) -> LMS_EncodedTag | LMS_DecodedTag:
19
+ end = reader.tell() + param_size
20
+
21
+ if config is None:
22
+ parameters = _read_encoded_parameters(reader, param_size)
23
+
24
+ if parameters is None:
25
+ return LMS_EncodedTag(group_index, tag_index)
26
+
27
+ return LMS_EncodedTag(group_index, tag_index, parameters)
28
+
29
+ definition = config.get_definition_by_indexes(group_index, tag_index)
30
+
31
+ # If the parameters were omitted from the definition but the tag still has defined parameters, read them.
32
+ # This is to account for encoded tags that group have tag names attatched.
33
+ if definition.parameters is None and param_size:
34
+ parameters = _read_encoded_parameters(reader, param_size)
35
+ return LMS_EncodedTag(
36
+ group_index,
37
+ tag_index,
38
+ parameters,
39
+ definition.group_name,
40
+ definition.tag_name,
41
+ )
42
+ else:
43
+ parameters = _read_decoded_parameters(reader, definition)
44
+
45
+ reader.seek(end)
46
+ return LMS_DecodedTag(
47
+ group_index, tag_index, definition.group_name, definition.tag_name, parameters
48
+ )
49
+
50
+
51
+ def write_tag(writer: FileWriter, tag: LMS_TagBase) -> None:
52
+ tag_indicator = b"\x0e" + (b"\x00" * (writer.encoding.width - 1))
53
+ if writer.big_endian:
54
+ tag_indicator = tag_indicator[::-1]
55
+
56
+ writer.write_bytes(tag_indicator)
57
+ writer.write_uint16(tag.group_index)
58
+ writer.write_uint16(tag.tag_index)
59
+
60
+ if tag.parameters is None:
61
+ writer.write_uint16(0)
62
+ return None
63
+
64
+ if isinstance(tag, LMS_EncodedTag):
65
+ _write_encoded_parameters(writer, tag.parameters)
66
+ else:
67
+ _write_decoded_parameters(writer, tag)
68
+
69
+ return None
70
+
71
+
72
+ # --
73
+ def _read_encoded_parameters(reader: FileReader, param_size: int) -> list[str] | None:
74
+ if param_size == 0:
75
+ return None
76
+
77
+ hex_parameters = reader.read_bytes(param_size).hex().upper()
78
+ encoded_parameters = [
79
+ hex_parameters[i : i + 2] for i in range(0, len(hex_parameters), 2)
80
+ ]
81
+ return encoded_parameters
82
+
83
+
84
+ def _write_encoded_parameters(writer: FileWriter, parameters: list[str] | None) -> None:
85
+ writer.write_uint16(len(parameters))
86
+ for param in parameters:
87
+ writer.write_bytes(bytes.fromhex(param))
88
+
89
+
90
+ def _read_decoded_parameters(
91
+ reader: FileReader, definition: TagDefinition
92
+ ) -> LMS_FieldMap:
93
+ parameters = {}
94
+ for param in definition.parameters:
95
+ param_offset = reader.tell()
96
+ try:
97
+ if param.datatype is LMS_DataType.STRING:
98
+ value = LMS_Field(reader.read_len_string_variable_encoding(), param)
99
+ else:
100
+ value = read_field(reader, param)
101
+ # There could be multiple errors related to reading, share the extra info but display the original exception
102
+ except Exception as e:
103
+ raise LMS_TagReadingError(
104
+ f"An error occured reading tag '[{definition.group_name}:{definition.tag_name}]', parameter '{param.name}' at offset {param_offset}"
105
+ ) from e
106
+
107
+ parameters[param.name] = value
108
+ return parameters
109
+
110
+
111
+ def _write_decoded_parameters(writer: FileWriter, tag: LMS_DecodedTag) -> None:
112
+ param_size = 0
113
+
114
+ # Tags are padded by 0xCD if the size is not aligned to the encoding
115
+ # This can occur before a string parameter, or at the end of the tag.
116
+ # Set a flag in order to be able to pad the first string correctly
117
+ needs_padding = False
118
+ first_string = True
119
+
120
+ for field in tag.parameters.values():
121
+ if field.datatype is LMS_DataType.STRING:
122
+ param_size += 2 + len(field.value) * writer.encoding.width
123
+ else:
124
+ param_size += field.datatype.stream_size
125
+
126
+ if param_size % 2 == 1:
127
+ needs_padding = True
128
+ param_size += 1
129
+
130
+ writer.write_uint16(param_size)
131
+ for field in tag.parameters.values():
132
+ try:
133
+ if field.datatype is LMS_DataType.STRING:
134
+ if first_string and needs_padding:
135
+ writer.write_bytes(b"\xcd")
136
+ first_string = False
137
+ needs_padding = False
138
+
139
+ writer.write_uint16(len(field.value) * writer.encoding.width)
140
+ writer.write_variable_encoding_string(field.value, False)
141
+ else:
142
+ write_field(writer, field)
143
+ except Exception as e:
144
+ raise LMS_TagWritingException(
145
+ f"An error occured writing tag '{tag}', parameter '{field.name}' at offset {writer.tell()}!"
146
+ ) from e
147
+
148
+ if needs_padding:
149
+ writer.write_bytes(b"\xcd")