blob: 6ba2d2a677fc4a5de5aca5241fc7f114a626f652 [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
Alex Kleine191ed62020-02-27 15:59:55 -070015import os
16
Mike Frysinger2c024062021-05-22 15:43:22 -040017from chromite.third_party.google.protobuf import json_format
Alex Kleine191ed62020-02-27 15:59:55 -070018
Alex Klein7b44c5b2020-06-30 10:50:35 -060019from chromite.lib import cros_logging as logging
Alex Kleine191ed62020-02-27 15:59:55 -070020from chromite.lib import osutils
21
Mike Frysingera9b30c72020-04-18 03:36:03 -040022
Alex Kleine191ed62020-02-27 15:59:55 -070023FORMAT_BINARY = 1
24FORMAT_JSON = 2
25VALID_FORMATS = (FORMAT_BINARY, FORMAT_JSON)
26
27
28class Error(Exception):
29 """Base error class for the module."""
30
31
32class InvalidHandlerError(Error):
33 """Raised when a message handler has no input/output argument when needed."""
34
35
36class InvalidInputFileError(Error):
37 """Raised when the input file cannot be read."""
38
39
40class InvalidInputFormatError(Error):
41 """Raised when the passed input protobuf can't be parsed."""
42
43
44class InvalidOutputFileError(Error):
45 """Raised when the output file cannot be written."""
46
47
48class UnknownHandlerError(Error):
49 """Raised when a valid type has not been implemented yet.
50
51 This should only ever be raised when under active development.
52 See: get_message_handler.
53 """
54
55
56def get_message_handler(path, msg_format):
57 """Get a message handler to handle the given message format."""
58 assert msg_format in VALID_FORMATS
59
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 elif msg_format == FORMAT_JSON:
69 return MessageHandler(
70 path=path,
71 serializer=JsonSerializer(),
72 binary=False,
73 input_arg='--input-json',
74 output_arg='--output-json',
75 config_arg='--config-json')
76 else:
77 # Unexpected. Your new format type needs a case in this function if
78 # you got this error.
79 raise UnknownHandlerError('Unknown format type.')
80
81
82class Serializer(object):
83 """Base (and null) serializer class."""
84
85 def deserialize(self, data, message):
86 """Deserialize the data into the given message.
87
88 Args:
89 data (str): The message data to deserialize.
90 message (google.protobuf.Message): The message to load the data into.
91 """
Alex Kleine191ed62020-02-27 15:59:55 -070092
93 # pylint: disable=unused-argument
94 def serialize(self, message):
95 """Serialize the message data.
96
97 Args:
98 message (google.protobuf.Message): The message to be serialized.
99
100 Returns:
101 str: The message's serialized data.
102 """
103 return ''
104
105
106class BinarySerializer(Serializer):
107 """Protobuf binary serializer class."""
108
109 def deserialize(self, data, message):
110 """Deserialize the data into the given message.
111
112 See: Serializer.deserialize
113 """
114 message.ParseFromString(data)
115
116 def serialize(self, message):
117 """Serialize the message data.
118
119 See: Serializer.serialize
120 """
121 return message.SerializeToString()
122
123
124class JsonSerializer(Serializer):
125 """Protobuf json serializer class."""
126
127 def deserialize(self, data, message):
128 """Deserialize the data into the given message.
129
130 See: Serializer.deserialize
131 """
132 try:
133 json_format.Parse(data, message, ignore_unknown_fields=True)
134 except json_format.ParseError as e:
135 raise InvalidInputFormatError('Unable to parse the input json: %s' % e)
136
137 def serialize(self, message):
138 """Serialize the message data.
139
140 See: Serializer.serialize
141 """
142 return json_format.MessageToJson(
Alex Klein9aba2222020-04-13 13:43:08 -0600143 message, sort_keys=True, use_integers_for_enums=True) or '{}'
Alex Kleine191ed62020-02-27 15:59:55 -0700144
145
146class MessageHandler(object):
147 """Class to handle message (de)serialization to and from files.
148
149 The class is fairly tightly coupled to the build api, but we currently have
150 no other projected use cases for this, so it's handy. In particular, if we
151 scrap the "maintain the same input/output/config serialization when reexecing
152 inside the chroot" convention, this implementation is much less useful and
153 can be fairly trivially generalized.
154
155 The instance's path is the primary path the message handler was built for.
156 For the Build API, this means one of the input/output/config arguments. In
157 practice, it's largely a convenience/shortcut so we don't have to either
158 track which input files are what types (which we know from the argument used
159 to pass them in), or create another containing data class for the
160 functionality provided by the handler and serializer classes and the build
161 api data.
162
163 Examples:
164 message_handler = MessageHandler(path, ...)
165 message = ...
166 # Parse path into message.
167 message_handler.read_into(message)
168 # Write message to a different file.
169 message_handler.write_into(message, path=other_path)
170 """
171
172 def __init__(self, path, serializer, binary, input_arg, output_arg,
173 config_arg):
174 """MessageHandler init.
175
176 Args:
177 path (str): The path to the main file associated with this handler.
178 serializer (Serializer): The serializer to be used for the messages.
179 binary (bool): Whether the serialized content is binary.
180 input_arg (str): The --input-x argument used for this type. Used for
181 reexecution inside the chroot.
182 output_arg (str): The --output-x argument used for this type. Used for
183 reexecution inside the chroot.
184 config_arg (str): The --config-x argument used for this type. Used for
185 reexecution inside the chroot.
186 """
187 self.path = path
188 self.serializer = serializer
189 self.read_mode = 'rb' if binary else 'r'
190 self.write_mode = 'wb' if binary else 'w'
191 self.input_arg = input_arg
192 self.output_arg = output_arg
193 self.config_arg = config_arg
194
195 def read_into(self, message, path=None):
196 """Read a file containing serialized data into a message.
197
198 Args:
199 message (google.protobuf.Message): The message to populate.
200 path (str|None): A path to read. Uses the instance's path when not given.
201
202 Raises:
203 InvalidInputFileError: When a path has not been given, does not exist,
204 or cannot be read.
205 """
Alex Klein7b44c5b2020-06-30 10:50:35 -0600206 target_path = path or self.path
207 if not target_path:
Alex Kleine191ed62020-02-27 15:59:55 -0700208 raise InvalidInputFileError('No input file has been specified.')
Alex Klein7b44c5b2020-06-30 10:50:35 -0600209 if not os.path.exists(target_path):
Alex Kleine191ed62020-02-27 15:59:55 -0700210 raise InvalidInputFileError('The input file does not exist.')
211
212 try:
Alex Klein7b44c5b2020-06-30 10:50:35 -0600213 content = osutils.ReadFile(target_path, mode=self.read_mode)
Alex Kleine191ed62020-02-27 15:59:55 -0700214 except IOError as e:
215 raise InvalidInputFileError('Unable to read input file: %s' % e)
216
Alex Klein7b44c5b2020-06-30 10:50:35 -0600217 if content:
218 self.serializer.deserialize(content, message)
219 else:
220 logging.warning('No content found in %s to deserialize.', target_path)
Alex Kleine191ed62020-02-27 15:59:55 -0700221
222 def write_from(self, message, path=None):
223 """Write serialized data from the message to a file.
224
225 Args:
226 message (google.protobuf.Message): The message to serialize and persist.
227 path (str|None): An optional override of the instance's path.
228
229 Raises:
230 InvalidOutputFileError: When no path given, or it cannot be written to.
231 """
232 if not path and not self.path:
233 raise InvalidOutputFileError('No output file has been specified.')
234
235 try:
236 osutils.WriteFile(
237 path or self.path,
238 self.serializer.serialize(message),
239 mode=self.write_mode)
240 except IOError as e:
241 raise InvalidOutputFileError('Cannot write output file: %s' % e)