blob: 85098723926c648eb0b937b4ffa90ad652bdf065 [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
Alex Kleine191ed62020-02-27 15:59:55 -070036class InvalidInputFileError(Error):
Alex Klein1699fab2022-09-08 08:46:06 -060037 """Raised when the input file cannot be read."""
Alex Kleine191ed62020-02-27 15:59:55 -070038
39
40class InvalidInputFormatError(Error):
Alex Klein1699fab2022-09-08 08:46:06 -060041 """Raised when the passed input protobuf can't be parsed."""
Alex Kleine191ed62020-02-27 15:59:55 -070042
43
44class InvalidOutputFileError(Error):
Alex Klein1699fab2022-09-08 08:46:06 -060045 """Raised when the output file cannot be written."""
Alex Kleine191ed62020-02-27 15:59:55 -070046
47
48class UnknownHandlerError(Error):
Alex Klein1699fab2022-09-08 08:46:06 -060049 """Raised when a valid type has not been implemented yet.
Alex Kleine191ed62020-02-27 15:59:55 -070050
Alex Klein1699fab2022-09-08 08:46:06 -060051 This should only ever be raised when under active development.
52 See: get_message_handler.
53 """
Alex Kleine191ed62020-02-27 15:59:55 -070054
55
56def get_message_handler(path, msg_format):
Alex Klein1699fab2022-09-08 08:46:06 -060057 """Get a message handler to handle the given message format."""
58 assert msg_format in VALID_FORMATS
Alex Kleine191ed62020-02-27 15:59:55 -070059
Alex Klein1699fab2022-09-08 08:46:06 -060060 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 Kleine191ed62020-02-27 15:59:55 -070082
83
Alex Klein074f94f2023-06-22 10:32:06 -060084class Serializer:
Alex Klein1699fab2022-09-08 08:46:06 -060085 """Base (and null) serializer class."""
Alex Kleine191ed62020-02-27 15:59:55 -070086
Alex Klein1699fab2022-09-08 08:46:06 -060087 def deserialize(self, data: str, message: "google.protobuf.Message"):
88 """Deserialize the data into the given message.
Alex Kleine191ed62020-02-27 15:59:55 -070089
Alex Klein1699fab2022-09-08 08:46:06 -060090 Args:
Alex Kleina0442682022-10-10 13:47:38 -060091 data: The message data to deserialize.
92 message: The message to load the data into.
Alex Klein1699fab2022-09-08 08:46:06 -060093 """
Alex Kleine191ed62020-02-27 15:59:55 -070094
Alex Klein1699fab2022-09-08 08:46:06 -060095 # pylint: disable=unused-argument
96 def serialize(self, message: "google.protobuf.Message") -> str:
97 """Serialize the message data.
Alex Kleine191ed62020-02-27 15:59:55 -070098
Alex Klein1699fab2022-09-08 08:46:06 -060099 Args:
Alex Kleina0442682022-10-10 13:47:38 -0600100 message: The message to be serialized.
Alex Kleine191ed62020-02-27 15:59:55 -0700101
Alex Klein1699fab2022-09-08 08:46:06 -0600102 Returns:
Alex Kleina0442682022-10-10 13:47:38 -0600103 The message's serialized data.
Alex Klein1699fab2022-09-08 08:46:06 -0600104 """
105 return ""
Alex Kleine191ed62020-02-27 15:59:55 -0700106
107
108class BinarySerializer(Serializer):
Alex Klein1699fab2022-09-08 08:46:06 -0600109 """Protobuf binary serializer class."""
Alex Kleine191ed62020-02-27 15:59:55 -0700110
Alex Klein1699fab2022-09-08 08:46:06 -0600111 def deserialize(self, data, message):
112 """Deserialize the data into the given message.
Alex Kleine191ed62020-02-27 15:59:55 -0700113
Alex Klein1699fab2022-09-08 08:46:06 -0600114 See: Serializer.deserialize
115 """
116 message.ParseFromString(data)
Alex Kleine191ed62020-02-27 15:59:55 -0700117
Alex Klein1699fab2022-09-08 08:46:06 -0600118 def serialize(self, message):
119 """Serialize the message data.
Alex Kleine191ed62020-02-27 15:59:55 -0700120
Alex Klein1699fab2022-09-08 08:46:06 -0600121 See: Serializer.serialize
122 """
123 return message.SerializeToString()
Alex Kleine191ed62020-02-27 15:59:55 -0700124
125
126class JsonSerializer(Serializer):
Alex Klein1699fab2022-09-08 08:46:06 -0600127 """Protobuf json serializer class."""
Alex Kleine191ed62020-02-27 15:59:55 -0700128
Alex Klein1699fab2022-09-08 08:46:06 -0600129 def deserialize(self, data, message):
130 """Deserialize the data into the given message.
Alex Kleine191ed62020-02-27 15:59:55 -0700131
Alex Klein1699fab2022-09-08 08:46:06 -0600132 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 Kleine191ed62020-02-27 15:59:55 -0700140
Alex Klein1699fab2022-09-08 08:46:06 -0600141 def serialize(self, message):
142 """Serialize the message data.
Alex Kleine191ed62020-02-27 15:59:55 -0700143
Alex Klein1699fab2022-09-08 08:46:06 -0600144 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 Kleine191ed62020-02-27 15:59:55 -0700152
153
Alex Klein074f94f2023-06-22 10:32:06 -0600154class MessageHandler:
Alex Klein1699fab2022-09-08 08:46:06 -0600155 """Class to handle message (de)serialization to and from files.
Alex Kleine191ed62020-02-27 15:59:55 -0700156
Alex Klein1699fab2022-09-08 08:46:06 -0600157 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 Klein54c891a2023-01-24 10:45:41 -0700159 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 Kleine191ed62020-02-27 15:59:55 -0700162
Alex Klein1699fab2022-09-08 08:46:06 -0600163 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 Kleine191ed62020-02-27 15:59:55 -0700170
Alex Klein1699fab2022-09-08 08:46:06 -0600171 Examples:
Alex Kleina0442682022-10-10 13:47:38 -0600172 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 Kleine191ed62020-02-27 15:59:55 -0700178 """
Alex Kleine191ed62020-02-27 15:59:55 -0700179
Alex Klein1699fab2022-09-08 08:46:06 -0600180 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 Kleine191ed62020-02-27 15:59:55 -0700190
Alex Klein1699fab2022-09-08 08:46:06 -0600191 Args:
Alex Kleina0442682022-10-10 13:47:38 -0600192 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 Klein1699fab2022-09-08 08:46:06 -0600201 """
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 Kleine191ed62020-02-27 15:59:55 -0700209
Alex Klein1699fab2022-09-08 08:46:06 -0600210 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 Kleine191ed62020-02-27 15:59:55 -0700214
Alex Klein1699fab2022-09-08 08:46:06 -0600215 Args:
Alex Kleina0442682022-10-10 13:47:38 -0600216 message: The message to populate.
217 path: A path to read. Uses the instance's path when not given.
Alex Kleine191ed62020-02-27 15:59:55 -0700218
Alex Klein1699fab2022-09-08 08:46:06 -0600219 Raises:
Alex Kleina0442682022-10-10 13:47:38 -0600220 InvalidInputFileError: When a path has not been given, does not
221 exist, or cannot be read.
Alex Klein1699fab2022-09-08 08:46:06 -0600222 """
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 Kleine191ed62020-02-27 15:59:55 -0700228
Alex Klein1699fab2022-09-08 08:46:06 -0600229 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 Kleine191ed62020-02-27 15:59:55 -0700233
Alex Klein1699fab2022-09-08 08:46:06 -0600234 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 Kleine191ed62020-02-27 15:59:55 -0700240
Alex Klein1699fab2022-09-08 08:46:06 -0600241 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 Kleine191ed62020-02-27 15:59:55 -0700245
Alex Klein1699fab2022-09-08 08:46:06 -0600246 Args:
Alex Kleina0442682022-10-10 13:47:38 -0600247 message: The message to serialize and persist.
248 path: An optional override of the instance's path.
Alex Klein1699fab2022-09-08 08:46:06 -0600249
250 Raises:
Alex Klein54c891a2023-01-24 10:45:41 -0700251 InvalidOutputFileError: When no path given, or the path cannot be
252 written to.
Alex Klein1699fab2022-09-08 08:46:06 -0600253 """
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)