1#!/usr/bin/python2
2
3#
4# Copyright (C) 2015 The Android Open Source Project
5#
6# Licensed under the Apache License, Version 2.0 (the "License");
7# you may not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10#      http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS,
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
17#
18
19"""A C++ code generator for printing protobufs which use the LITE_RUNTIME.
20
21Normally printing a protobuf would be done with Message::DebugString(). However,
22this is not available when using only MessageLite. This script generates code to
23emulate Message::DebugString() without using reflection. The input must be a
24valid .proto file.
25
26Usage: proto_print.py [--subdir=foo] <bar.proto>
27
28Files named print_bar_proto.h and print_bar_proto.cc will be created in the
29current working directory.
30"""
31
32from __future__ import print_function
33
34import argparse
35import collections
36from datetime import date
37import os
38import re
39import subprocess
40
41
42# Holds information about a protobuf message field.
43#
44# Attributes:
45#   repeated: Whether the field is a repeated field.
46#   type_: The type of the field. E.g. int32.
47#   name: The name of the field.
48Field = collections.namedtuple('Field', 'repeated type_ name')
49
50
51class Message(object):
52  """Holds information about a protobuf message.
53
54  Attributes:
55    name: The name of the message.
56    fields: A list of Field tuples.
57  """
58
59  def __init__(self, name):
60    """Initializes a Message instance.
61
62    Args:
63      name: The protobuf message name.
64    """
65    self.name = name
66    self.fields = []
67
68  def AddField(self, attribute, field_type, field_name):
69    """Adds a new field to the message.
70
71    Args:
72      attribute: This should be 'optional', 'required', or 'repeated'.
73      field_type: The type of the field. E.g. int32.
74      field_name: The name of the field.
75    """
76    self.fields.append(Field(repeated=attribute == 'repeated',
77                             type_=field_type, name=field_name))
78
79
80class Enum(object):
81  """Holds information about a protobuf enum.
82
83  Attributes:
84    name: The name of the enum.
85    values: A list of enum value names.
86  """
87
88  def __init__(self, name):
89    """Initializes a Enum instance.
90
91    Args:
92      name: The protobuf enum name.
93    """
94    self.name = name
95    self.values = []
96
97  def AddValue(self, value_name):
98    """Adds a new value to the enum.
99
100    Args:
101      value_name: The name of the value.
102    """
103    self.values.append(value_name)
104
105
106def ParseProto(input_file):
107  """Parses a proto file and returns a tuple of parsed information.
108
109  Args:
110    input_file: The proto file to parse.
111
112  Returns:
113    A tuple in the form (package, imports, messages, enums) where
114      package: A string holding the proto package.
115      imports: A list of strings holding proto imports.
116      messages: A list of Message objects; one for each message in the proto.
117      enums: A list of Enum objects; one for each enum in the proto.
118  """
119  package = ''
120  imports = []
121  messages = []
122  enums = []
123  current_message_stack = []
124  current_enum = None
125  package_re = re.compile(r'package\s+(\w+);')
126  import_re = re.compile(r'import\s+"([\w]*/)*(\w+).proto";')
127  message_re = re.compile(r'message\s+(\w+)\s*{')
128  field_re = re.compile(r'(optional|required|repeated)\s+(\w+)\s+(\w+)\s*=')
129  enum_re = re.compile(r'enum\s+(\w+)\s*{')
130  enum_value_re = re.compile(r'(\w+)\s*=')
131  for line in input_file:
132    line = line.strip()
133    if not line or line.startswith('//'):
134      continue
135    # Close off the current scope. Enums first because they can't be nested.
136    if line == '}':
137      if current_enum:
138        enums.append(current_enum)
139        current_enum = None
140      if current_message_stack:
141        messages.append(current_message_stack.pop())
142      continue
143    # Look for a message definition.
144    match = message_re.search(line)
145    if match:
146      prefix = ''
147      if current_message_stack:
148        prefix = '::'.join([m.name for m in current_message_stack]) + '::'
149      current_message_stack.append(Message(prefix + match.group(1)))
150      continue
151    # Look for a message field definition.
152    if current_message_stack:
153      match = field_re.search(line)
154      if match:
155        current_message_stack[-1].AddField(match.group(1),
156                                           match.group(2),
157                                           match.group(3))
158        continue
159    # Look for an enum definition.
160    match = enum_re.search(line)
161    if match:
162      current_enum = Enum(match.group(1))
163      continue
164    # Look for an enum value.
165    if current_enum:
166      match = enum_value_re.search(line)
167      if match:
168        current_enum.AddValue(match.group(1))
169        continue
170    # Look for a package statement.
171    match = package_re.search(line)
172    if match:
173      package = match.group(1)
174    # Look for an import statement.
175    match = import_re.search(line)
176    if match:
177      imports.append(match.group(2))
178  return package, imports, messages, enums
179
180
181def GenerateFileHeaders(proto_name, package, imports, subdir, header_file_name,
182                        header_file, impl_file):
183  """Generates and prints file headers.
184
185  Args:
186    proto_name: The name of the proto file.
187    package: The protobuf package.
188    imports: A list of imported protos.
189    subdir: The --subdir arg.
190    header_file_name: The header file name.
191    header_file: The header file handle, open for writing.
192    impl_file: The implementation file handle, open for writing.
193  """
194  if subdir:
195    guard_name = '%s_%s_PRINT_%s_PROTO_H_' % (package.upper(),
196                                              subdir.upper(),
197                                              proto_name.upper())
198    package_with_subdir = '%s/%s' % (package, subdir)
199  else:
200    guard_name = '%s_PRINT_%s_PROTO_H_' % (package.upper(), proto_name.upper())
201    package_with_subdir = package
202  includes = '\n'.join(
203      ['#include "%(package_with_subdir)s/print_%(import)s_proto.h"' % {
204          'package_with_subdir': package_with_subdir,
205          'import': current_import} for current_import in imports])
206  header = """\
207// Copyright %(year)s The Chromium OS Authors. All rights reserved.
208// Use of this source code is governed by a BSD-style license that can be
209// found in the LICENSE file.
210
211// THIS CODE IS GENERATED.
212
213#ifndef %(guard_name)s
214#define %(guard_name)s
215
216#include <string>
217
218#include "%(package_with_subdir)s/%(proto)s.pb.h"
219
220namespace %(package)s {
221""" % {'year': date.today().year,
222       'guard_name': guard_name,
223       'package': package,
224       'proto': proto_name,
225       'package_with_subdir': package_with_subdir}
226  impl = """\
227// Copyright %(year)s The Chromium OS Authors. All rights reserved.
228// Use of this source code is governed by a BSD-style license that can be
229// found in the LICENSE file.
230
231// THIS CODE IS GENERATED.
232
233#include "%(package_with_subdir)s/%(header_file_name)s"
234
235#include <string>
236
237#include <base/strings/string_number_conversions.h>
238#include <base/strings/stringprintf.h>
239
240%(includes)s
241
242namespace %(package)s {
243""" % {'year': date.today().year,
244       'package': package,
245       'package_with_subdir': package_with_subdir,
246       'header_file_name': header_file_name,
247       'includes': includes}
248
249  header_file.write(header)
250  impl_file.write(impl)
251
252
253def GenerateFileFooters(proto_name, package, subdir, header_file, impl_file):
254  """Generates and prints file footers.
255
256  Args:
257    proto_name: The name of the proto file.
258    package: The protobuf package.
259    subdir: The --subdir arg.
260    header_file: The header file handle, open for writing.
261    impl_file: The implementation file handle, open for writing.
262  """
263  if subdir:
264    guard_name = '%s_%s_PRINT_%s_PROTO_H_' % (package.upper(),
265                                              subdir.upper(),
266                                              proto_name.upper())
267  else:
268    guard_name = '%s_PRINT_%s_PROTO_H_' % (package.upper(), proto_name.upper())
269  header = """
270
271}  // namespace %(package)s
272
273#endif  // %(guard_name)s
274""" % {'guard_name': guard_name, 'package': package}
275  impl = """
276}  // namespace %(package)s
277""" % {'package': package}
278
279  header_file.write(header)
280  impl_file.write(impl)
281
282
283def GenerateEnumPrinter(enum, header_file, impl_file):
284  """Generates and prints a function to print an enum value.
285
286  Args:
287    enum: An Enum instance.
288    header_file: The header file handle, open for writing.
289    impl_file: The implementation file handle, open for writing.
290  """
291  declare = """
292std::string GetProtoDebugStringWithIndent(%(name)s value, int indent_size);
293std::string GetProtoDebugString(%(name)s value);""" % {'name': enum.name}
294  define_begin = """
295std::string GetProtoDebugString(%(name)s value) {
296  return GetProtoDebugStringWithIndent(value, 0);
297}
298
299std::string GetProtoDebugStringWithIndent(%(name)s value, int indent_size) {
300""" % {'name': enum.name}
301  define_end = """
302  return "<unknown>";
303}
304"""
305  condition = """
306  if (value == %(value_name)s) {
307    return "%(value_name)s";
308  }"""
309
310  header_file.write(declare)
311  impl_file.write(define_begin)
312  for value_name in enum.values:
313    impl_file.write(condition % {'value_name': value_name})
314  impl_file.write(define_end)
315
316
317def GenerateMessagePrinter(message, header_file, impl_file):
318  """Generates and prints a function to print a message.
319
320  Args:
321    message: A Message instance.
322    header_file: The header file handle, open for writing.
323    impl_file: The implementation file handle, open for writing.
324  """
325  declare = """
326std::string GetProtoDebugStringWithIndent(const %(name)s& value,
327                                          int indent_size);
328std::string GetProtoDebugString(const %(name)s& value);""" % {'name':
329                                                              message.name}
330  define_begin = """
331std::string GetProtoDebugString(const %(name)s& value) {
332  return GetProtoDebugStringWithIndent(value, 0);
333}
334
335std::string GetProtoDebugStringWithIndent(const %(name)s& value,
336                                          int indent_size) {
337  std::string indent(indent_size, ' ');
338  std::string output = base::StringPrintf("[%%s] {\\n",
339                                          value.GetTypeName().c_str());
340""" % {'name': message.name}
341  define_end = """
342  output += indent + "}\\n";
343  return output;
344}
345"""
346  singular_field = """
347  if (value.has_%(name)s()) {
348    output += indent + "  %(name)s: ";
349    base::StringAppendF(&output, %(format)s);
350    output += "\\n";
351  }"""
352  repeated_field = """
353  output += indent + "  %(name)s: {";
354  for (int i = 0; i < value.%(name)s_size(); ++i) {
355    if (i > 0) {
356      base::StringAppendF(&output, ", ");
357    }
358    base::StringAppendF(&output, %(format)s);
359  }
360  output += "}\\n";"""
361  singular_field_get = 'value.%(name)s()'
362  repeated_field_get = 'value.%(name)s(i)'
363  formats = {'bool': '"%%s", %(value)s ? "true" : "false"',
364             'int32': '"%%d", %(value)s',
365             'int64': '"%%ld", %(value)s',
366             'uint32': '"%%u (0x%%08X)", %(value)s, %(value)s',
367             'uint64': '"%%lu (0x%%016X)", %(value)s, %(value)s',
368             'string': '"%%s", %(value)s.c_str()',
369             'bytes': """"%%s", base::HexEncode(%(value)s.data(),
370                                                %(value)s.size()).c_str()"""}
371  subtype_format = ('"%%s", GetProtoDebugStringWithIndent(%(value)s, '
372                    'indent_size + 2).c_str()')
373
374  header_file.write(declare)
375  impl_file.write(define_begin)
376  for field in message.fields:
377    if field.repeated:
378      value_get = repeated_field_get % {'name': field.name}
379      field_code = repeated_field
380    else:
381      value_get = singular_field_get % {'name': field.name}
382      field_code = singular_field
383    if field.type_ in formats:
384      value_format = formats[field.type_] % {'value': value_get}
385    else:
386      value_format = subtype_format % {'value': value_get}
387    impl_file.write(field_code % {'name': field.name,
388                                  'format': value_format})
389  impl_file.write(define_end)
390
391
392def FormatFile(filename):
393  subprocess.call(['clang-format', '-i', '-style=Chromium', filename])
394
395
396def main():
397  parser = argparse.ArgumentParser(description='print proto code generator')
398  parser.add_argument('input_file')
399  parser.add_argument('--subdir', default='')
400  args = parser.parse_args()
401  with open(args.input_file) as input_file:
402    package, imports, messages, enums = ParseProto(input_file)
403  proto_name = os.path.basename(args.input_file).rsplit('.', 1)[0]
404  header_file_name = 'print_%s_proto.h' % proto_name
405  impl_file_name = 'print_%s_proto.cc' % proto_name
406  with open(header_file_name, 'w') as header_file:
407    with open(impl_file_name, 'w') as impl_file:
408      GenerateFileHeaders(proto_name, package, imports, args.subdir,
409                          header_file_name, header_file, impl_file)
410      for enum in enums:
411        GenerateEnumPrinter(enum, header_file, impl_file)
412      for message in messages:
413        GenerateMessagePrinter(message, header_file, impl_file)
414      GenerateFileFooters(proto_name, package, args.subdir, header_file,
415                          impl_file)
416  FormatFile(header_file_name)
417  FormatFile(impl_file_name)
418
419if __name__ == '__main__':
420  main()
421