1#!/usr/bin/python
2# Copyright 2016 The Chromium 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"""
7 Convert the ASCII download_file_types.asciipb proto into a binary resource.
8
9 We generate a separate variant of the binary proto for each platform,
10 each which contains only the values that platform needs.
11"""
12
13import os
14import re
15import sys
16
17# Import the binary proto generator. Walks up to the root of the source tree
18# which is five directories above, and the finds the protobufs directory from
19# there.
20proto_generator_path = os.path.normpath(os.path.join(os.path.abspath(__file__),
21    *[os.path.pardir] * 5 + ['components/resources/protobufs']))
22sys.path.insert(0, proto_generator_path)
23from binary_proto_generator import BinaryProtoGenerator
24
25# Map of platforms for which we can generate binary protos.
26# This must be run after the custom imports.
27#   key: type-name
28#   value: proto-platform_type (int)
29def PlatformTypes():
30  return {
31    "android": download_file_types_pb2.DownloadFileType.PLATFORM_ANDROID,
32    "chromeos": download_file_types_pb2.DownloadFileType.PLATFORM_CHROME_OS,
33    "linux": download_file_types_pb2.DownloadFileType.PLATFORM_LINUX,
34    "bsd": download_file_types_pb2.DownloadFileType.PLATFORM_LINUX,
35    "mac": download_file_types_pb2.DownloadFileType.PLATFORM_MAC,
36    "win": download_file_types_pb2.DownloadFileType.PLATFORM_WINDOWS,
37  }
38
39
40def PrunePlatformSettings(file_type, default_settings, platform_type):
41  # Modify this file_type's platform_settings by keeping the only the
42  # best one for this platform_type. In order of preference:
43  #   * Exact match to platform_type
44  #   * PLATFORM_ANY entry
45  #   * or copy from the default file type.
46
47  last_platform = -1
48  setting_match = None
49  for s in file_type.platform_settings:
50    # Enforce: sorted and no dups (signs of mistakes).
51    assert last_platform < s.platform, (
52        "Extension '%s' has duplicate or out of order platform: '%s'" %
53        (file_type.extension, s.platform))
54    last_platform = s.platform
55
56    # Pick the most specific match.
57    if ((s.platform == platform_type) or
58        (s.platform == download_file_types_pb2.DownloadFileType.PLATFORM_ANY and
59         setting_match is None)):
60      setting_match = s
61
62  # If platform_settings was empty, we'll fill in from the default
63  if setting_match is None:
64    assert default_settings is not None, (
65        "Missing default settings for platform %d" % platform_type)
66    setting_match = default_settings
67
68  # Now clear out the full list and replace it with 1 entry.
69  del file_type.platform_settings[:]
70  new_setting = file_type.platform_settings.add()
71  new_setting.CopyFrom(setting_match)
72  new_setting.ClearField('platform')
73
74
75def FilterPbForPlatform(full_pb, platform_type):
76  """ Return a filtered protobuf for this platform_type """
77  assert type(platform_type) is int, "Bad platform_type type"
78
79  new_pb = download_file_types_pb2.DownloadFileTypeConfig();
80  new_pb.CopyFrom(full_pb)
81
82  # Ensure there's only one platform_settings for the default.
83  PrunePlatformSettings(new_pb.default_file_type, None, platform_type)
84
85  # This can be extended if we want to match weird extensions.
86  # Just no dots, non-UTF8, or uppercase chars.
87  invalid_char_re = re.compile('[^a-z0-9_-]')
88
89  # Filter platform_settings for each type.
90  uma_values_used = set()
91  extensions_used = set()
92  for file_type in new_pb.file_types:
93    assert not invalid_char_re.search(file_type.extension), (
94        "File extension '%s' contains non alpha-num-dash chars" % (
95            file_type.extension))
96    assert file_type.extension not in extensions_used, (
97        "Duplicate extension '%s'" % file_type.extension)
98    extensions_used.add(file_type.extension)
99
100    assert file_type.uma_value not in uma_values_used, (
101        "Extension '%s' reused UMA value %d." % (
102            file_type.extension, file_type.uma_value))
103    uma_values_used.add(file_type.uma_value)
104
105    # Modify file_type to include only the best match platform_setting.
106    PrunePlatformSettings(
107        file_type, new_pb.default_file_type.platform_settings[0], platform_type)
108
109  return new_pb
110
111
112def FilterForPlatformAndWrite(full_pb, platform_type, outfile):
113  """ Filter and write out a file for this platform """
114  # Filter it
115  filtered_pb = FilterPbForPlatform(full_pb, platform_type);
116  # Serialize it
117  binary_pb_str = filtered_pb.SerializeToString()
118  # Write it to disk
119  open(outfile, 'wb').write(binary_pb_str)
120
121
122def MakeSubDirs(outfile):
123  """ Make the subdirectories needed to create file |outfile| """
124  dirname = os.path.dirname(outfile)
125  if not os.path.exists(dirname):
126    os.makedirs(dirname)
127
128
129class DownloadFileTypeProtoGenerator(BinaryProtoGenerator):
130
131  def ImportProtoModule(self):
132    import download_file_types_pb2
133    globals()['download_file_types_pb2'] = download_file_types_pb2
134
135  def EmptyProtoInstance(self):
136    return download_file_types_pb2.DownloadFileTypeConfig()
137
138  def ValidatePb(self, opts, pb):
139    """ Validate the basic values of the protobuf.  The
140        file_type_policies_unittest.cc will also validate it by platform,
141        but this will catch errors earlier.
142    """
143    assert pb.version_id > 0;
144    assert pb.sampled_ping_probability >= 0.0;
145    assert pb.sampled_ping_probability <= 1.0;
146    assert len(pb.default_file_type.platform_settings) >= 1;
147    assert len(pb.file_types) > 1;
148
149  def ProcessPb(self, opts, pb):
150    """ Generate one or more binary protos using the parsed proto. """
151    if opts.type is not None:
152      # Just one platform type
153      platform_enum = PlatformTypes()[opts.type]
154      outfile = os.path.join(opts.outdir, opts.outbasename)
155      FilterForPlatformAndWrite(pb, platform_enum, outfile)
156    else:
157      # Make a separate file for each platform
158      for platform_type, platform_enum in PlatformTypes().iteritems():
159        # e.g. .../all/77/chromeos/download_file_types.pb
160        outfile = os.path.join(opts.outdir,
161                               str(pb.version_id),
162                               platform_type,
163                               opts.outbasename)
164        MakeSubDirs(outfile)
165        FilterForPlatformAndWrite(pb, platform_enum, outfile)
166
167  def AddCommandLineOptions(self, parser):
168    parser.add_option('-a', '--all', action="store_true", default=False,
169                      help='Write a separate file for every platform. '
170                      'Outfile must have a %d for version and %s for platform.')
171    parser.add_option('-t', '--type',
172                      help='The platform type. One of android, chromeos, ' +
173                      'linux, bsd, mac, win')
174
175  def AddExtraCommandLineArgsForVirtualEnvRun(self, opts, command):
176    if opts.type is not None:
177      command += ['-t', opts.type]
178    if opts.all:
179      command += ['-a']
180
181  def VerifyArgs(self, opts):
182    if (not opts.all and opts.type not in PlatformTypes()):
183      print "ERROR: Unknown platform type '%s'" % opts.type
184      self.opt_parser.print_help()
185      return False
186
187    if (bool(opts.all) == bool(opts.type)):
188      print "ERROR: Need exactly one of --type or --all"
189      self.opt_parser.print_help()
190      return False
191    return True
192
193def main():
194  return DownloadFileTypeProtoGenerator().Run()
195
196if __name__ == '__main__':
197  sys.exit(main())
198