blob: bb8fd0ad47708acbf537a2bd9fddfbba6b12c386 [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
Alex Klein7b44c5b2020-06-30 10:50:35 -060023from chromite.lib import cros_logging as logging
Alex Kleine191ed62020-02-27 15:59:55 -070024from chromite.lib import osutils
25
Mike Frysingera9b30c72020-04-18 03:36:03 -040026
27assert sys.version_info >= (3, 6), 'This module requires Python 3.6+'
28
29
Alex Kleine191ed62020-02-27 15:59:55 -070030FORMAT_BINARY = 1
31FORMAT_JSON = 2
32VALID_FORMATS = (FORMAT_BINARY, FORMAT_JSON)
33
34
35class Error(Exception):
36 """Base error class for the module."""
37
38
39class InvalidHandlerError(Error):
40 """Raised when a message handler has no input/output argument when needed."""
41
42
43class InvalidInputFileError(Error):
44 """Raised when the input file cannot be read."""
45
46
47class InvalidInputFormatError(Error):
48 """Raised when the passed input protobuf can't be parsed."""
49
50
51class InvalidOutputFileError(Error):
52 """Raised when the output file cannot be written."""
53
54
55class UnknownHandlerError(Error):
56 """Raised when a valid type has not been implemented yet.
57
58 This should only ever be raised when under active development.
59 See: get_message_handler.
60 """
61
62
63def get_message_handler(path, msg_format):
64 """Get a message handler to handle the given message format."""
65 assert msg_format in VALID_FORMATS
66
67 if msg_format == FORMAT_BINARY:
68 return MessageHandler(
69 path=path,
70 serializer=BinarySerializer(),
71 binary=True,
72 input_arg='--input-binary',
73 output_arg='--output-binary',
74 config_arg='--config-binary')
75 elif msg_format == FORMAT_JSON:
76 return MessageHandler(
77 path=path,
78 serializer=JsonSerializer(),
79 binary=False,
80 input_arg='--input-json',
81 output_arg='--output-json',
82 config_arg='--config-json')
83 else:
84 # Unexpected. Your new format type needs a case in this function if
85 # you got this error.
86 raise UnknownHandlerError('Unknown format type.')
87
88
89class Serializer(object):
90 """Base (and null) serializer class."""
91
92 def deserialize(self, data, message):
93 """Deserialize the data into the given message.
94
95 Args:
96 data (str): The message data to deserialize.
97 message (google.protobuf.Message): The message to load the data into.
98 """
Alex Kleine191ed62020-02-27 15:59:55 -070099
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 """
Alex Klein7b44c5b2020-06-30 10:50:35 -0600213 target_path = path or self.path
214 if not target_path:
Alex Kleine191ed62020-02-27 15:59:55 -0700215 raise InvalidInputFileError('No input file has been specified.')
Alex Klein7b44c5b2020-06-30 10:50:35 -0600216 if not os.path.exists(target_path):
Alex Kleine191ed62020-02-27 15:59:55 -0700217 raise InvalidInputFileError('The input file does not exist.')
218
219 try:
Alex Klein7b44c5b2020-06-30 10:50:35 -0600220 content = osutils.ReadFile(target_path, mode=self.read_mode)
Alex Kleine191ed62020-02-27 15:59:55 -0700221 except IOError as e:
222 raise InvalidInputFileError('Unable to read input file: %s' % e)
223
Alex Klein7b44c5b2020-06-30 10:50:35 -0600224 if content:
225 self.serializer.deserialize(content, message)
226 else:
227 logging.warning('No content found in %s to deserialize.', target_path)
Alex Kleine191ed62020-02-27 15:59:55 -0700228
229 def write_from(self, message, path=None):
230 """Write serialized data from the message to a file.
231
232 Args:
233 message (google.protobuf.Message): The message to serialize and persist.
234 path (str|None): An optional override of the instance's path.
235
236 Raises:
237 InvalidOutputFileError: When no path given, or it cannot be written to.
238 """
239 if not path and not self.path:
240 raise InvalidOutputFileError('No output file has been specified.')
241
242 try:
243 osutils.WriteFile(
244 path or self.path,
245 self.serializer.serialize(message),
246 mode=self.write_mode)
247 except IOError as e:
248 raise InvalidOutputFileError('Cannot write output file: %s' % e)