Mike Frysinger | f1ba7ad | 2022-09-12 05:42:57 -0400 | [diff] [blame] | 1 | # Copyright 2020 The ChromiumOS Authors |
Alex Klein | e191ed6 | 2020-02-27 15:59:55 -0700 | [diff] [blame] | 2 | # Use of this source code is governed by a BSD-style license that can be |
| 3 | # found in the LICENSE file. |
| 4 | |
| 5 | """Protobuf message utilities. |
| 6 | |
| 7 | The Serializer classes are adapters to standardize the reading and writing of |
| 8 | different protobuf message serialization formats to and from a message. |
| 9 | |
| 10 | The base MessageHandler class encapsulates the functionality of reading |
| 11 | a file containing serialized data into a protobuf message instance, and |
| 12 | writing serialized data from a message instance out to a file. |
| 13 | """ |
| 14 | |
Chris McDonald | 1672ddb | 2021-07-21 11:48:23 -0600 | [diff] [blame] | 15 | import logging |
Alex Klein | e191ed6 | 2020-02-27 15:59:55 -0700 | [diff] [blame] | 16 | import os |
Kevin Shelton | d0275c8 | 2022-08-05 01:57:51 +0000 | [diff] [blame] | 17 | from typing import Optional, TYPE_CHECKING |
Alex Klein | e191ed6 | 2020-02-27 15:59:55 -0700 | [diff] [blame] | 18 | |
Mike Frysinger | 2c02406 | 2021-05-22 15:43:22 -0400 | [diff] [blame] | 19 | from chromite.third_party.google.protobuf import json_format |
Alex Klein | e191ed6 | 2020-02-27 15:59:55 -0700 | [diff] [blame] | 20 | |
| 21 | from chromite.lib import osutils |
| 22 | |
Mike Frysinger | a9b30c7 | 2020-04-18 03:36:03 -0400 | [diff] [blame] | 23 | |
Kevin Shelton | d0275c8 | 2022-08-05 01:57:51 +0000 | [diff] [blame] | 24 | if TYPE_CHECKING: |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 25 | from chromite.third_party import google |
Kevin Shelton | d0275c8 | 2022-08-05 01:57:51 +0000 | [diff] [blame] | 26 | |
Alex Klein | e191ed6 | 2020-02-27 15:59:55 -0700 | [diff] [blame] | 27 | FORMAT_BINARY = 1 |
| 28 | FORMAT_JSON = 2 |
| 29 | VALID_FORMATS = (FORMAT_BINARY, FORMAT_JSON) |
| 30 | |
| 31 | |
| 32 | class Error(Exception): |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 33 | """Base error class for the module.""" |
Alex Klein | e191ed6 | 2020-02-27 15:59:55 -0700 | [diff] [blame] | 34 | |
| 35 | |
Alex Klein | e191ed6 | 2020-02-27 15:59:55 -0700 | [diff] [blame] | 36 | class InvalidInputFileError(Error): |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 37 | """Raised when the input file cannot be read.""" |
Alex Klein | e191ed6 | 2020-02-27 15:59:55 -0700 | [diff] [blame] | 38 | |
| 39 | |
| 40 | class InvalidInputFormatError(Error): |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 41 | """Raised when the passed input protobuf can't be parsed.""" |
Alex Klein | e191ed6 | 2020-02-27 15:59:55 -0700 | [diff] [blame] | 42 | |
| 43 | |
| 44 | class InvalidOutputFileError(Error): |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 45 | """Raised when the output file cannot be written.""" |
Alex Klein | e191ed6 | 2020-02-27 15:59:55 -0700 | [diff] [blame] | 46 | |
| 47 | |
| 48 | class UnknownHandlerError(Error): |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 49 | """Raised when a valid type has not been implemented yet. |
Alex Klein | e191ed6 | 2020-02-27 15:59:55 -0700 | [diff] [blame] | 50 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 51 | This should only ever be raised when under active development. |
| 52 | See: get_message_handler. |
| 53 | """ |
Alex Klein | e191ed6 | 2020-02-27 15:59:55 -0700 | [diff] [blame] | 54 | |
| 55 | |
| 56 | def get_message_handler(path, msg_format): |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 57 | """Get a message handler to handle the given message format.""" |
| 58 | assert msg_format in VALID_FORMATS |
Alex Klein | e191ed6 | 2020-02-27 15:59:55 -0700 | [diff] [blame] | 59 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 60 | if msg_format == FORMAT_BINARY: |
| 61 | return MessageHandler( |
| 62 | path=path, |
| 63 | serializer=BinarySerializer(), |
| 64 | binary=True, |
| 65 | input_arg="--input-binary", |
| 66 | output_arg="--output-binary", |
| 67 | config_arg="--config-binary", |
| 68 | ) |
| 69 | elif msg_format == FORMAT_JSON: |
| 70 | return MessageHandler( |
| 71 | path=path, |
| 72 | serializer=JsonSerializer(), |
| 73 | binary=False, |
| 74 | input_arg="--input-json", |
| 75 | output_arg="--output-json", |
| 76 | config_arg="--config-json", |
| 77 | ) |
| 78 | else: |
| 79 | # Unexpected. Your new format type needs a case in this function if |
| 80 | # you got this error. |
| 81 | raise UnknownHandlerError("Unknown format type.") |
Alex Klein | e191ed6 | 2020-02-27 15:59:55 -0700 | [diff] [blame] | 82 | |
| 83 | |
Alex Klein | 074f94f | 2023-06-22 10:32:06 -0600 | [diff] [blame^] | 84 | class Serializer: |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 85 | """Base (and null) serializer class.""" |
Alex Klein | e191ed6 | 2020-02-27 15:59:55 -0700 | [diff] [blame] | 86 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 87 | def deserialize(self, data: str, message: "google.protobuf.Message"): |
| 88 | """Deserialize the data into the given message. |
Alex Klein | e191ed6 | 2020-02-27 15:59:55 -0700 | [diff] [blame] | 89 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 90 | Args: |
Alex Klein | a044268 | 2022-10-10 13:47:38 -0600 | [diff] [blame] | 91 | data: The message data to deserialize. |
| 92 | message: The message to load the data into. |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 93 | """ |
Alex Klein | e191ed6 | 2020-02-27 15:59:55 -0700 | [diff] [blame] | 94 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 95 | # pylint: disable=unused-argument |
| 96 | def serialize(self, message: "google.protobuf.Message") -> str: |
| 97 | """Serialize the message data. |
Alex Klein | e191ed6 | 2020-02-27 15:59:55 -0700 | [diff] [blame] | 98 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 99 | Args: |
Alex Klein | a044268 | 2022-10-10 13:47:38 -0600 | [diff] [blame] | 100 | message: The message to be serialized. |
Alex Klein | e191ed6 | 2020-02-27 15:59:55 -0700 | [diff] [blame] | 101 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 102 | Returns: |
Alex Klein | a044268 | 2022-10-10 13:47:38 -0600 | [diff] [blame] | 103 | The message's serialized data. |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 104 | """ |
| 105 | return "" |
Alex Klein | e191ed6 | 2020-02-27 15:59:55 -0700 | [diff] [blame] | 106 | |
| 107 | |
| 108 | class BinarySerializer(Serializer): |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 109 | """Protobuf binary serializer class.""" |
Alex Klein | e191ed6 | 2020-02-27 15:59:55 -0700 | [diff] [blame] | 110 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 111 | def deserialize(self, data, message): |
| 112 | """Deserialize the data into the given message. |
Alex Klein | e191ed6 | 2020-02-27 15:59:55 -0700 | [diff] [blame] | 113 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 114 | See: Serializer.deserialize |
| 115 | """ |
| 116 | message.ParseFromString(data) |
Alex Klein | e191ed6 | 2020-02-27 15:59:55 -0700 | [diff] [blame] | 117 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 118 | def serialize(self, message): |
| 119 | """Serialize the message data. |
Alex Klein | e191ed6 | 2020-02-27 15:59:55 -0700 | [diff] [blame] | 120 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 121 | See: Serializer.serialize |
| 122 | """ |
| 123 | return message.SerializeToString() |
Alex Klein | e191ed6 | 2020-02-27 15:59:55 -0700 | [diff] [blame] | 124 | |
| 125 | |
| 126 | class JsonSerializer(Serializer): |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 127 | """Protobuf json serializer class.""" |
Alex Klein | e191ed6 | 2020-02-27 15:59:55 -0700 | [diff] [blame] | 128 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 129 | def deserialize(self, data, message): |
| 130 | """Deserialize the data into the given message. |
Alex Klein | e191ed6 | 2020-02-27 15:59:55 -0700 | [diff] [blame] | 131 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 132 | See: Serializer.deserialize |
| 133 | """ |
| 134 | try: |
| 135 | json_format.Parse(data, message, ignore_unknown_fields=True) |
| 136 | except json_format.ParseError as e: |
| 137 | raise InvalidInputFormatError( |
| 138 | "Unable to parse the input json: %s" % e |
| 139 | ) |
Alex Klein | e191ed6 | 2020-02-27 15:59:55 -0700 | [diff] [blame] | 140 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 141 | def serialize(self, message): |
| 142 | """Serialize the message data. |
Alex Klein | e191ed6 | 2020-02-27 15:59:55 -0700 | [diff] [blame] | 143 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 144 | See: Serializer.serialize |
| 145 | """ |
| 146 | return ( |
| 147 | json_format.MessageToJson( |
| 148 | message, sort_keys=True, use_integers_for_enums=True |
| 149 | ) |
| 150 | or "{}" |
| 151 | ) |
Alex Klein | e191ed6 | 2020-02-27 15:59:55 -0700 | [diff] [blame] | 152 | |
| 153 | |
Alex Klein | 074f94f | 2023-06-22 10:32:06 -0600 | [diff] [blame^] | 154 | class MessageHandler: |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 155 | """Class to handle message (de)serialization to and from files. |
Alex Klein | e191ed6 | 2020-02-27 15:59:55 -0700 | [diff] [blame] | 156 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 157 | The class is fairly tightly coupled to the build api, but we currently have |
| 158 | no other projected use cases for this, so it's handy. In particular, if we |
Alex Klein | 54c891a | 2023-01-24 10:45:41 -0700 | [diff] [blame] | 159 | scrap the "maintain the same input/output/config serialization when |
| 160 | reexecing inside the chroot" convention, this implementation is much less |
| 161 | useful and can be fairly trivially generalized. |
Alex Klein | e191ed6 | 2020-02-27 15:59:55 -0700 | [diff] [blame] | 162 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 163 | The instance's path is the primary path the message handler was built for. |
| 164 | For the Build API, this means one of the input/output/config arguments. In |
| 165 | practice, it's largely a convenience/shortcut so we don't have to either |
| 166 | track which input files are what types (which we know from the argument used |
| 167 | to pass them in), or create another containing data class for the |
| 168 | functionality provided by the handler and serializer classes and the build |
| 169 | api data. |
Alex Klein | e191ed6 | 2020-02-27 15:59:55 -0700 | [diff] [blame] | 170 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 171 | Examples: |
Alex Klein | a044268 | 2022-10-10 13:47:38 -0600 | [diff] [blame] | 172 | message_handler = MessageHandler(path, ...) |
| 173 | message = ... |
| 174 | # Parse path into message. |
| 175 | message_handler.read_into(message) |
| 176 | # Write message to a different file. |
| 177 | message_handler.write_into(message, path=other_path) |
Alex Klein | e191ed6 | 2020-02-27 15:59:55 -0700 | [diff] [blame] | 178 | """ |
Alex Klein | e191ed6 | 2020-02-27 15:59:55 -0700 | [diff] [blame] | 179 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 180 | def __init__( |
| 181 | self, |
| 182 | path: str, |
| 183 | serializer: Serializer, |
| 184 | binary: bool, |
| 185 | input_arg: str, |
| 186 | output_arg: str, |
| 187 | config_arg: str, |
| 188 | ): |
| 189 | """MessageHandler init. |
Alex Klein | e191ed6 | 2020-02-27 15:59:55 -0700 | [diff] [blame] | 190 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 191 | Args: |
Alex Klein | a044268 | 2022-10-10 13:47:38 -0600 | [diff] [blame] | 192 | path: The path to the main file associated with this handler. |
| 193 | serializer: The serializer to be used for the messages. |
| 194 | binary: Whether the serialized content is binary. |
| 195 | input_arg: The --input-x argument used for this type. Used for |
| 196 | reexecution inside the chroot. |
| 197 | output_arg: The --output-x argument used for this type. Used for |
| 198 | reexecution inside the chroot. |
| 199 | config_arg: The --config-x argument used for this type. Used for |
| 200 | reexecution inside the chroot. |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 201 | """ |
| 202 | self.path = path |
| 203 | self.serializer = serializer |
| 204 | self.read_mode = "rb" if binary else "r" |
| 205 | self.write_mode = "wb" if binary else "w" |
| 206 | self.input_arg = input_arg |
| 207 | self.output_arg = output_arg |
| 208 | self.config_arg = config_arg |
Alex Klein | e191ed6 | 2020-02-27 15:59:55 -0700 | [diff] [blame] | 209 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 210 | def read_into( |
| 211 | self, message: "google.protobuf.Message", path: Optional[str] = None |
| 212 | ): |
| 213 | """Read a file containing serialized data into a message. |
Alex Klein | e191ed6 | 2020-02-27 15:59:55 -0700 | [diff] [blame] | 214 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 215 | Args: |
Alex Klein | a044268 | 2022-10-10 13:47:38 -0600 | [diff] [blame] | 216 | message: The message to populate. |
| 217 | path: A path to read. Uses the instance's path when not given. |
Alex Klein | e191ed6 | 2020-02-27 15:59:55 -0700 | [diff] [blame] | 218 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 219 | Raises: |
Alex Klein | a044268 | 2022-10-10 13:47:38 -0600 | [diff] [blame] | 220 | InvalidInputFileError: When a path has not been given, does not |
| 221 | exist, or cannot be read. |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 222 | """ |
| 223 | target_path = path or self.path |
| 224 | if not target_path: |
| 225 | raise InvalidInputFileError("No input file has been specified.") |
| 226 | if not os.path.exists(target_path): |
| 227 | raise InvalidInputFileError("The input file does not exist.") |
Alex Klein | e191ed6 | 2020-02-27 15:59:55 -0700 | [diff] [blame] | 228 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 229 | try: |
| 230 | content = osutils.ReadFile(target_path, mode=self.read_mode) |
| 231 | except IOError as e: |
| 232 | raise InvalidInputFileError("Unable to read input file: %s" % e) |
Alex Klein | e191ed6 | 2020-02-27 15:59:55 -0700 | [diff] [blame] | 233 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 234 | if content: |
| 235 | self.serializer.deserialize(content, message) |
| 236 | else: |
| 237 | logging.warning( |
| 238 | "No content found in %s to deserialize.", target_path |
| 239 | ) |
Alex Klein | e191ed6 | 2020-02-27 15:59:55 -0700 | [diff] [blame] | 240 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 241 | def write_from( |
| 242 | self, message: "google.protobuf.Message", path: Optional[str] = None |
| 243 | ): |
| 244 | """Write serialized data from the message to a file. |
Alex Klein | e191ed6 | 2020-02-27 15:59:55 -0700 | [diff] [blame] | 245 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 246 | Args: |
Alex Klein | a044268 | 2022-10-10 13:47:38 -0600 | [diff] [blame] | 247 | message: The message to serialize and persist. |
| 248 | path: An optional override of the instance's path. |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 249 | |
| 250 | Raises: |
Alex Klein | 54c891a | 2023-01-24 10:45:41 -0700 | [diff] [blame] | 251 | InvalidOutputFileError: When no path given, or the path cannot be |
| 252 | written to. |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame] | 253 | """ |
| 254 | if not path and not self.path: |
| 255 | raise InvalidOutputFileError("No output file has been specified.") |
| 256 | |
| 257 | try: |
| 258 | osutils.WriteFile( |
| 259 | path or self.path, |
| 260 | self.serializer.serialize(message), |
| 261 | mode=self.write_mode, |
| 262 | ) |
| 263 | except IOError as e: |
| 264 | raise InvalidOutputFileError("Cannot write output file: %s" % e) |