blob: db8bcf22b596f508d638e559ec413d69deb7c346 [file] [log] [blame]
Mike Frysingerf1ba7ad2022-09-12 05:42:57 -04001# Copyright 2020 The ChromiumOS Authors
Alex Kleine191ed62020-02-27 15:59:55 -07002# 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
7The Serializer classes are adapters to standardize the reading and writing of
8different protobuf message serialization formats to and from a message.
9
10The base MessageHandler class encapsulates the functionality of reading
11a file containing serialized data into a protobuf message instance, and
12writing serialized data from a message instance out to a file.
13"""
14
Chris McDonald1672ddb2021-07-21 11:48:23 -060015import logging
Alex Kleine191ed62020-02-27 15:59:55 -070016import os
Kevin Sheltond0275c82022-08-05 01:57:51 +000017from typing import Optional, TYPE_CHECKING
Alex Kleine191ed62020-02-27 15:59:55 -070018
Mike Frysinger2c024062021-05-22 15:43:22 -040019from chromite.third_party.google.protobuf import json_format
Alex Kleine191ed62020-02-27 15:59:55 -070020
21from chromite.lib import osutils
22
Mike Frysingera9b30c72020-04-18 03:36:03 -040023
Kevin Sheltond0275c82022-08-05 01:57:51 +000024if TYPE_CHECKING:
Alex Klein1699fab2022-09-08 08:46:06 -060025 from chromite.third_party import google
Kevin Sheltond0275c82022-08-05 01:57:51 +000026
Alex Kleine191ed62020-02-27 15:59:55 -070027FORMAT_BINARY = 1
28FORMAT_JSON = 2
29VALID_FORMATS = (FORMAT_BINARY, FORMAT_JSON)
30
31
32class Error(Exception):
Alex Klein1699fab2022-09-08 08:46:06 -060033 """Base error class for the module."""
Alex Kleine191ed62020-02-27 15:59:55 -070034
35
36class InvalidHandlerError(Error):
Alex Klein1699fab2022-09-08 08:46:06 -060037 """Raised when a message handler has no input/output argument when needed."""
Alex Kleine191ed62020-02-27 15:59:55 -070038
39
40class InvalidInputFileError(Error):
Alex Klein1699fab2022-09-08 08:46:06 -060041 """Raised when the input file cannot be read."""
Alex Kleine191ed62020-02-27 15:59:55 -070042
43
44class InvalidInputFormatError(Error):
Alex Klein1699fab2022-09-08 08:46:06 -060045 """Raised when the passed input protobuf can't be parsed."""
Alex Kleine191ed62020-02-27 15:59:55 -070046
47
48class InvalidOutputFileError(Error):
Alex Klein1699fab2022-09-08 08:46:06 -060049 """Raised when the output file cannot be written."""
Alex Kleine191ed62020-02-27 15:59:55 -070050
51
52class UnknownHandlerError(Error):
Alex Klein1699fab2022-09-08 08:46:06 -060053 """Raised when a valid type has not been implemented yet.
Alex Kleine191ed62020-02-27 15:59:55 -070054
Alex Klein1699fab2022-09-08 08:46:06 -060055 This should only ever be raised when under active development.
56 See: get_message_handler.
57 """
Alex Kleine191ed62020-02-27 15:59:55 -070058
59
60def get_message_handler(path, msg_format):
Alex Klein1699fab2022-09-08 08:46:06 -060061 """Get a message handler to handle the given message format."""
62 assert msg_format in VALID_FORMATS
Alex Kleine191ed62020-02-27 15:59:55 -070063
Alex Klein1699fab2022-09-08 08:46:06 -060064 if msg_format == FORMAT_BINARY:
65 return MessageHandler(
66 path=path,
67 serializer=BinarySerializer(),
68 binary=True,
69 input_arg="--input-binary",
70 output_arg="--output-binary",
71 config_arg="--config-binary",
72 )
73 elif msg_format == FORMAT_JSON:
74 return MessageHandler(
75 path=path,
76 serializer=JsonSerializer(),
77 binary=False,
78 input_arg="--input-json",
79 output_arg="--output-json",
80 config_arg="--config-json",
81 )
82 else:
83 # Unexpected. Your new format type needs a case in this function if
84 # you got this error.
85 raise UnknownHandlerError("Unknown format type.")
Alex Kleine191ed62020-02-27 15:59:55 -070086
87
88class Serializer(object):
Alex Klein1699fab2022-09-08 08:46:06 -060089 """Base (and null) serializer class."""
Alex Kleine191ed62020-02-27 15:59:55 -070090
Alex Klein1699fab2022-09-08 08:46:06 -060091 def deserialize(self, data: str, message: "google.protobuf.Message"):
92 """Deserialize the data into the given message.
Alex Kleine191ed62020-02-27 15:59:55 -070093
Alex Klein1699fab2022-09-08 08:46:06 -060094 Args:
Alex Kleina0442682022-10-10 13:47:38 -060095 data: The message data to deserialize.
96 message: The message to load the data into.
Alex Klein1699fab2022-09-08 08:46:06 -060097 """
Alex Kleine191ed62020-02-27 15:59:55 -070098
Alex Klein1699fab2022-09-08 08:46:06 -060099 # pylint: disable=unused-argument
100 def serialize(self, message: "google.protobuf.Message") -> str:
101 """Serialize the message data.
Alex Kleine191ed62020-02-27 15:59:55 -0700102
Alex Klein1699fab2022-09-08 08:46:06 -0600103 Args:
Alex Kleina0442682022-10-10 13:47:38 -0600104 message: The message to be serialized.
Alex Kleine191ed62020-02-27 15:59:55 -0700105
Alex Klein1699fab2022-09-08 08:46:06 -0600106 Returns:
Alex Kleina0442682022-10-10 13:47:38 -0600107 The message's serialized data.
Alex Klein1699fab2022-09-08 08:46:06 -0600108 """
109 return ""
Alex Kleine191ed62020-02-27 15:59:55 -0700110
111
112class BinarySerializer(Serializer):
Alex Klein1699fab2022-09-08 08:46:06 -0600113 """Protobuf binary serializer class."""
Alex Kleine191ed62020-02-27 15:59:55 -0700114
Alex Klein1699fab2022-09-08 08:46:06 -0600115 def deserialize(self, data, message):
116 """Deserialize the data into the given message.
Alex Kleine191ed62020-02-27 15:59:55 -0700117
Alex Klein1699fab2022-09-08 08:46:06 -0600118 See: Serializer.deserialize
119 """
120 message.ParseFromString(data)
Alex Kleine191ed62020-02-27 15:59:55 -0700121
Alex Klein1699fab2022-09-08 08:46:06 -0600122 def serialize(self, message):
123 """Serialize the message data.
Alex Kleine191ed62020-02-27 15:59:55 -0700124
Alex Klein1699fab2022-09-08 08:46:06 -0600125 See: Serializer.serialize
126 """
127 return message.SerializeToString()
Alex Kleine191ed62020-02-27 15:59:55 -0700128
129
130class JsonSerializer(Serializer):
Alex Klein1699fab2022-09-08 08:46:06 -0600131 """Protobuf json serializer class."""
Alex Kleine191ed62020-02-27 15:59:55 -0700132
Alex Klein1699fab2022-09-08 08:46:06 -0600133 def deserialize(self, data, message):
134 """Deserialize the data into the given message.
Alex Kleine191ed62020-02-27 15:59:55 -0700135
Alex Klein1699fab2022-09-08 08:46:06 -0600136 See: Serializer.deserialize
137 """
138 try:
139 json_format.Parse(data, message, ignore_unknown_fields=True)
140 except json_format.ParseError as e:
141 raise InvalidInputFormatError(
142 "Unable to parse the input json: %s" % e
143 )
Alex Kleine191ed62020-02-27 15:59:55 -0700144
Alex Klein1699fab2022-09-08 08:46:06 -0600145 def serialize(self, message):
146 """Serialize the message data.
Alex Kleine191ed62020-02-27 15:59:55 -0700147
Alex Klein1699fab2022-09-08 08:46:06 -0600148 See: Serializer.serialize
149 """
150 return (
151 json_format.MessageToJson(
152 message, sort_keys=True, use_integers_for_enums=True
153 )
154 or "{}"
155 )
Alex Kleine191ed62020-02-27 15:59:55 -0700156
157
158class MessageHandler(object):
Alex Klein1699fab2022-09-08 08:46:06 -0600159 """Class to handle message (de)serialization to and from files.
Alex Kleine191ed62020-02-27 15:59:55 -0700160
Alex Klein1699fab2022-09-08 08:46:06 -0600161 The class is fairly tightly coupled to the build api, but we currently have
162 no other projected use cases for this, so it's handy. In particular, if we
163 scrap the "maintain the same input/output/config serialization when reexecing
164 inside the chroot" convention, this implementation is much less useful and
165 can be fairly trivially generalized.
Alex Kleine191ed62020-02-27 15:59:55 -0700166
Alex Klein1699fab2022-09-08 08:46:06 -0600167 The instance's path is the primary path the message handler was built for.
168 For the Build API, this means one of the input/output/config arguments. In
169 practice, it's largely a convenience/shortcut so we don't have to either
170 track which input files are what types (which we know from the argument used
171 to pass them in), or create another containing data class for the
172 functionality provided by the handler and serializer classes and the build
173 api data.
Alex Kleine191ed62020-02-27 15:59:55 -0700174
Alex Klein1699fab2022-09-08 08:46:06 -0600175 Examples:
Alex Kleina0442682022-10-10 13:47:38 -0600176 message_handler = MessageHandler(path, ...)
177 message = ...
178 # Parse path into message.
179 message_handler.read_into(message)
180 # Write message to a different file.
181 message_handler.write_into(message, path=other_path)
Alex Kleine191ed62020-02-27 15:59:55 -0700182 """
Alex Kleine191ed62020-02-27 15:59:55 -0700183
Alex Klein1699fab2022-09-08 08:46:06 -0600184 def __init__(
185 self,
186 path: str,
187 serializer: Serializer,
188 binary: bool,
189 input_arg: str,
190 output_arg: str,
191 config_arg: str,
192 ):
193 """MessageHandler init.
Alex Kleine191ed62020-02-27 15:59:55 -0700194
Alex Klein1699fab2022-09-08 08:46:06 -0600195 Args:
Alex Kleina0442682022-10-10 13:47:38 -0600196 path: The path to the main file associated with this handler.
197 serializer: The serializer to be used for the messages.
198 binary: Whether the serialized content is binary.
199 input_arg: The --input-x argument used for this type. Used for
200 reexecution inside the chroot.
201 output_arg: The --output-x argument used for this type. Used for
202 reexecution inside the chroot.
203 config_arg: The --config-x argument used for this type. Used for
204 reexecution inside the chroot.
Alex Klein1699fab2022-09-08 08:46:06 -0600205 """
206 self.path = path
207 self.serializer = serializer
208 self.read_mode = "rb" if binary else "r"
209 self.write_mode = "wb" if binary else "w"
210 self.input_arg = input_arg
211 self.output_arg = output_arg
212 self.config_arg = config_arg
Alex Kleine191ed62020-02-27 15:59:55 -0700213
Alex Klein1699fab2022-09-08 08:46:06 -0600214 def read_into(
215 self, message: "google.protobuf.Message", path: Optional[str] = None
216 ):
217 """Read a file containing serialized data into a message.
Alex Kleine191ed62020-02-27 15:59:55 -0700218
Alex Klein1699fab2022-09-08 08:46:06 -0600219 Args:
Alex Kleina0442682022-10-10 13:47:38 -0600220 message: The message to populate.
221 path: A path to read. Uses the instance's path when not given.
Alex Kleine191ed62020-02-27 15:59:55 -0700222
Alex Klein1699fab2022-09-08 08:46:06 -0600223 Raises:
Alex Kleina0442682022-10-10 13:47:38 -0600224 InvalidInputFileError: When a path has not been given, does not
225 exist, or cannot be read.
Alex Klein1699fab2022-09-08 08:46:06 -0600226 """
227 target_path = path or self.path
228 if not target_path:
229 raise InvalidInputFileError("No input file has been specified.")
230 if not os.path.exists(target_path):
231 raise InvalidInputFileError("The input file does not exist.")
Alex Kleine191ed62020-02-27 15:59:55 -0700232
Alex Klein1699fab2022-09-08 08:46:06 -0600233 try:
234 content = osutils.ReadFile(target_path, mode=self.read_mode)
235 except IOError as e:
236 raise InvalidInputFileError("Unable to read input file: %s" % e)
Alex Kleine191ed62020-02-27 15:59:55 -0700237
Alex Klein1699fab2022-09-08 08:46:06 -0600238 if content:
239 self.serializer.deserialize(content, message)
240 else:
241 logging.warning(
242 "No content found in %s to deserialize.", target_path
243 )
Alex Kleine191ed62020-02-27 15:59:55 -0700244
Alex Klein1699fab2022-09-08 08:46:06 -0600245 def write_from(
246 self, message: "google.protobuf.Message", path: Optional[str] = None
247 ):
248 """Write serialized data from the message to a file.
Alex Kleine191ed62020-02-27 15:59:55 -0700249
Alex Klein1699fab2022-09-08 08:46:06 -0600250 Args:
Alex Kleina0442682022-10-10 13:47:38 -0600251 message: The message to serialize and persist.
252 path: An optional override of the instance's path.
Alex Klein1699fab2022-09-08 08:46:06 -0600253
254 Raises:
Alex Kleina0442682022-10-10 13:47:38 -0600255 InvalidOutputFileError: When no path given, or it cannot be written to.
Alex Klein1699fab2022-09-08 08:46:06 -0600256 """
257 if not path and not self.path:
258 raise InvalidOutputFileError("No output file has been specified.")
259
260 try:
261 osutils.WriteFile(
262 path or self.path,
263 self.serializer.serialize(message),
264 mode=self.write_mode,
265 )
266 except IOError as e:
267 raise InvalidOutputFileError("Cannot write output file: %s" % e)