blob: d3d748fd08b5d4b28827d85fb721d67b31ef8fbd [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 """
Alex Kleine191ed62020-02-27 15:59:55 -070098
99 # pylint: disable=unused-argument
100 def serialize(self, message):
101 """Serialize the message data.
102
103 Args:
104 message (google.protobuf.Message): The message to be serialized.
105
106 Returns:
107 str: The message's serialized data.
108 """
109 return ''
110
111
112class BinarySerializer(Serializer):
113 """Protobuf binary serializer class."""
114
115 def deserialize(self, data, message):
116 """Deserialize the data into the given message.
117
118 See: Serializer.deserialize
119 """
120 message.ParseFromString(data)
121
122 def serialize(self, message):
123 """Serialize the message data.
124
125 See: Serializer.serialize
126 """
127 return message.SerializeToString()
128
129
130class JsonSerializer(Serializer):
131 """Protobuf json serializer class."""
132
133 def deserialize(self, data, message):
134 """Deserialize the data into the given message.
135
136 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('Unable to parse the input json: %s' % e)
142
143 def serialize(self, message):
144 """Serialize the message data.
145
146 See: Serializer.serialize
147 """
148 return json_format.MessageToJson(
Alex Klein9aba2222020-04-13 13:43:08 -0600149 message, sort_keys=True, use_integers_for_enums=True) or '{}'
Alex Kleine191ed62020-02-27 15:59:55 -0700150
151
152class MessageHandler(object):
153 """Class to handle message (de)serialization to and from files.
154
155 The class is fairly tightly coupled to the build api, but we currently have
156 no other projected use cases for this, so it's handy. In particular, if we
157 scrap the "maintain the same input/output/config serialization when reexecing
158 inside the chroot" convention, this implementation is much less useful and
159 can be fairly trivially generalized.
160
161 The instance's path is the primary path the message handler was built for.
162 For the Build API, this means one of the input/output/config arguments. In
163 practice, it's largely a convenience/shortcut so we don't have to either
164 track which input files are what types (which we know from the argument used
165 to pass them in), or create another containing data class for the
166 functionality provided by the handler and serializer classes and the build
167 api data.
168
169 Examples:
170 message_handler = MessageHandler(path, ...)
171 message = ...
172 # Parse path into message.
173 message_handler.read_into(message)
174 # Write message to a different file.
175 message_handler.write_into(message, path=other_path)
176 """
177
178 def __init__(self, path, serializer, binary, input_arg, output_arg,
179 config_arg):
180 """MessageHandler init.
181
182 Args:
183 path (str): The path to the main file associated with this handler.
184 serializer (Serializer): The serializer to be used for the messages.
185 binary (bool): Whether the serialized content is binary.
186 input_arg (str): The --input-x argument used for this type. Used for
187 reexecution inside the chroot.
188 output_arg (str): The --output-x argument used for this type. Used for
189 reexecution inside the chroot.
190 config_arg (str): The --config-x argument used for this type. Used for
191 reexecution inside the chroot.
192 """
193 self.path = path
194 self.serializer = serializer
195 self.read_mode = 'rb' if binary else 'r'
196 self.write_mode = 'wb' if binary else 'w'
197 self.input_arg = input_arg
198 self.output_arg = output_arg
199 self.config_arg = config_arg
200
201 def read_into(self, message, path=None):
202 """Read a file containing serialized data into a message.
203
204 Args:
205 message (google.protobuf.Message): The message to populate.
206 path (str|None): A path to read. Uses the instance's path when not given.
207
208 Raises:
209 InvalidInputFileError: When a path has not been given, does not exist,
210 or cannot be read.
211 """
212 if not path and not self.path:
213 raise InvalidInputFileError('No input file has been specified.')
214 if not os.path.exists(path or self.path):
215 raise InvalidInputFileError('The input file does not exist.')
216
217 try:
218 content = osutils.ReadFile(path or self.path, mode=self.read_mode)
219 except IOError as e:
220 raise InvalidInputFileError('Unable to read input file: %s' % e)
221
222 self.serializer.deserialize(content, message)
223
224 def write_from(self, message, path=None):
225 """Write serialized data from the message to a file.
226
227 Args:
228 message (google.protobuf.Message): The message to serialize and persist.
229 path (str|None): An optional override of the instance's path.
230
231 Raises:
232 InvalidOutputFileError: When no path given, or it cannot be written to.
233 """
234 if not path and not self.path:
235 raise InvalidOutputFileError('No output file has been specified.')
236
237 try:
238 osutils.WriteFile(
239 path or self.path,
240 self.serializer.serialize(message),
241 mode=self.write_mode)
242 except IOError as e:
243 raise InvalidOutputFileError('Cannot write output file: %s' % e)