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