blob: 559a888ea874ae20147cec1ce6a0ae2017f4dbf8 [file] [log] [blame]
Alex Kleine191ed62020-02-27 15:59:55 -07001# Copyright 2020 The Chromium OS Authors. All rights reserved.
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
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:
25 from chromite.third_party import google
26
Alex Kleine191ed62020-02-27 15:59:55 -070027FORMAT_BINARY = 1
28FORMAT_JSON = 2
29VALID_FORMATS = (FORMAT_BINARY, FORMAT_JSON)
30
31
32class Error(Exception):
33 """Base error class for the module."""
34
35
36class InvalidHandlerError(Error):
37 """Raised when a message handler has no input/output argument when needed."""
38
39
40class InvalidInputFileError(Error):
41 """Raised when the input file cannot be read."""
42
43
44class InvalidInputFormatError(Error):
45 """Raised when the passed input protobuf can't be parsed."""
46
47
48class InvalidOutputFileError(Error):
49 """Raised when the output file cannot be written."""
50
51
52class UnknownHandlerError(Error):
53 """Raised when a valid type has not been implemented yet.
54
55 This should only ever be raised when under active development.
56 See: get_message_handler.
57 """
58
59
60def get_message_handler(path, msg_format):
61 """Get a message handler to handle the given message format."""
62 assert msg_format in VALID_FORMATS
63
64 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 elif msg_format == FORMAT_JSON:
73 return MessageHandler(
74 path=path,
75 serializer=JsonSerializer(),
76 binary=False,
77 input_arg='--input-json',
78 output_arg='--output-json',
79 config_arg='--config-json')
80 else:
81 # Unexpected. Your new format type needs a case in this function if
82 # you got this error.
83 raise UnknownHandlerError('Unknown format type.')
84
85
86class Serializer(object):
87 """Base (and null) serializer class."""
88
Kevin Sheltond0275c82022-08-05 01:57:51 +000089 def deserialize(self, data: str, message: 'google.protobuf.Message'):
Alex Kleine191ed62020-02-27 15:59:55 -070090 """Deserialize the data into the given message.
91
92 Args:
Kevin Sheltond0275c82022-08-05 01:57:51 +000093 data: The message data to deserialize.
94 message: The message to load the data into.
Alex Kleine191ed62020-02-27 15:59:55 -070095 """
Alex Kleine191ed62020-02-27 15:59:55 -070096
97 # pylint: disable=unused-argument
Kevin Sheltond0275c82022-08-05 01:57:51 +000098 def serialize(self, message: 'google.protobuf.Message') -> str:
Alex Kleine191ed62020-02-27 15:59:55 -070099 """Serialize the message data.
100
101 Args:
Kevin Sheltond0275c82022-08-05 01:57:51 +0000102 message: The message to be serialized.
Alex Kleine191ed62020-02-27 15:59:55 -0700103
104 Returns:
Kevin Sheltond0275c82022-08-05 01:57:51 +0000105 The message's serialized data.
Alex Kleine191ed62020-02-27 15:59:55 -0700106 """
107 return ''
108
109
110class BinarySerializer(Serializer):
111 """Protobuf binary serializer class."""
112
113 def deserialize(self, data, message):
114 """Deserialize the data into the given message.
115
116 See: Serializer.deserialize
117 """
118 message.ParseFromString(data)
119
120 def serialize(self, message):
121 """Serialize the message data.
122
123 See: Serializer.serialize
124 """
125 return message.SerializeToString()
126
127
128class JsonSerializer(Serializer):
129 """Protobuf json serializer class."""
130
131 def deserialize(self, data, message):
132 """Deserialize the data into the given message.
133
134 See: Serializer.deserialize
135 """
136 try:
137 json_format.Parse(data, message, ignore_unknown_fields=True)
138 except json_format.ParseError as e:
139 raise InvalidInputFormatError('Unable to parse the input json: %s' % e)
140
141 def serialize(self, message):
142 """Serialize the message data.
143
144 See: Serializer.serialize
145 """
146 return json_format.MessageToJson(
Alex Klein9aba2222020-04-13 13:43:08 -0600147 message, sort_keys=True, use_integers_for_enums=True) or '{}'
Alex Kleine191ed62020-02-27 15:59:55 -0700148
149
150class MessageHandler(object):
151 """Class to handle message (de)serialization to and from files.
152
153 The class is fairly tightly coupled to the build api, but we currently have
154 no other projected use cases for this, so it's handy. In particular, if we
155 scrap the "maintain the same input/output/config serialization when reexecing
156 inside the chroot" convention, this implementation is much less useful and
157 can be fairly trivially generalized.
158
159 The instance's path is the primary path the message handler was built for.
160 For the Build API, this means one of the input/output/config arguments. In
161 practice, it's largely a convenience/shortcut so we don't have to either
162 track which input files are what types (which we know from the argument used
163 to pass them in), or create another containing data class for the
164 functionality provided by the handler and serializer classes and the build
165 api data.
166
167 Examples:
168 message_handler = MessageHandler(path, ...)
169 message = ...
170 # Parse path into message.
171 message_handler.read_into(message)
172 # Write message to a different file.
173 message_handler.write_into(message, path=other_path)
174 """
175
Kevin Sheltond0275c82022-08-05 01:57:51 +0000176 def __init__(self, path: str, serializer: Serializer, binary: bool,
177 input_arg: str, output_arg: str, config_arg: str):
Alex Kleine191ed62020-02-27 15:59:55 -0700178 """MessageHandler init.
179
180 Args:
Kevin Sheltond0275c82022-08-05 01:57:51 +0000181 path: The path to the main file associated with this handler.
182 serializer: The serializer to be used for the messages.
183 binary: Whether the serialized content is binary.
184 input_arg: The --input-x argument used for this type. Used for
Alex Kleine191ed62020-02-27 15:59:55 -0700185 reexecution inside the chroot.
Kevin Sheltond0275c82022-08-05 01:57:51 +0000186 output_arg: The --output-x argument used for this type. Used for
Alex Kleine191ed62020-02-27 15:59:55 -0700187 reexecution inside the chroot.
Kevin Sheltond0275c82022-08-05 01:57:51 +0000188 config_arg: The --config-x argument used for this type. Used for
Alex Kleine191ed62020-02-27 15:59:55 -0700189 reexecution inside the chroot.
190 """
191 self.path = path
192 self.serializer = serializer
193 self.read_mode = 'rb' if binary else 'r'
194 self.write_mode = 'wb' if binary else 'w'
195 self.input_arg = input_arg
196 self.output_arg = output_arg
197 self.config_arg = config_arg
198
Kevin Sheltond0275c82022-08-05 01:57:51 +0000199 def read_into(self, message: 'google.protobuf.Message',
200 path: Optional[str] = None):
Alex Kleine191ed62020-02-27 15:59:55 -0700201 """Read a file containing serialized data into a message.
202
203 Args:
Kevin Sheltond0275c82022-08-05 01:57:51 +0000204 message: The message to populate.
205 path: A path to read. Uses the instance's path when not given.
Alex Kleine191ed62020-02-27 15:59:55 -0700206
207 Raises:
208 InvalidInputFileError: When a path has not been given, does not exist,
209 or cannot be read.
210 """
Alex Klein7b44c5b2020-06-30 10:50:35 -0600211 target_path = path or self.path
212 if not target_path:
Alex Kleine191ed62020-02-27 15:59:55 -0700213 raise InvalidInputFileError('No input file has been specified.')
Alex Klein7b44c5b2020-06-30 10:50:35 -0600214 if not os.path.exists(target_path):
Alex Kleine191ed62020-02-27 15:59:55 -0700215 raise InvalidInputFileError('The input file does not exist.')
216
217 try:
Alex Klein7b44c5b2020-06-30 10:50:35 -0600218 content = osutils.ReadFile(target_path, mode=self.read_mode)
Alex Kleine191ed62020-02-27 15:59:55 -0700219 except IOError as e:
220 raise InvalidInputFileError('Unable to read input file: %s' % e)
221
Alex Klein7b44c5b2020-06-30 10:50:35 -0600222 if content:
223 self.serializer.deserialize(content, message)
224 else:
225 logging.warning('No content found in %s to deserialize.', target_path)
Alex Kleine191ed62020-02-27 15:59:55 -0700226
Kevin Sheltond0275c82022-08-05 01:57:51 +0000227 def write_from(self, message: 'google.protobuf.Message',
228 path: Optional[str] = None):
Alex Kleine191ed62020-02-27 15:59:55 -0700229 """Write serialized data from the message to a file.
230
231 Args:
Kevin Sheltond0275c82022-08-05 01:57:51 +0000232 message: The message to serialize and persist.
233 path: An optional override of the instance's path.
Alex Kleine191ed62020-02-27 15:59:55 -0700234
235 Raises:
236 InvalidOutputFileError: When no path given, or it cannot be written to.
237 """
238 if not path and not self.path:
239 raise InvalidOutputFileError('No output file has been specified.')
240
241 try:
242 osutils.WriteFile(
243 path or self.path,
244 self.serializer.serialize(message),
245 mode=self.write_mode)
246 except IOError as e:
247 raise InvalidOutputFileError('Cannot write output file: %s' % e)