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