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