Script to push download_file_types.pb to all platforms, via Component Updater.

BUG=596555
CQ_INCLUDE_TRYBOTS=tryserver.chromium.linux:closure_compilation

Review-Url: https://codereview.chromium.org/2003323003
Cr-Commit-Position: refs/heads/master@{#396114}
diff --git a/chrome/browser/resources/safe_browsing/BUILD.gn b/chrome/browser/resources/safe_browsing/BUILD.gn
index 6793358..6149d24 100644
--- a/chrome/browser/resources/safe_browsing/BUILD.gn
+++ b/chrome/browser/resources/safe_browsing/BUILD.gn
@@ -2,6 +2,8 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+# TODO(nparker): reduce the duplication between these two, somehow.
+
 # Generate the binary proto form of "file_types" from the ascii proto.
 action("make_file_types_protobuf") {
   script = "gen_file_type_proto.py"
@@ -10,7 +12,8 @@
   # chrome/browser/browser_resources.grd will look for it.
 
   input_filename = "download_file_types.asciipb"
-  output_filename = "$target_gen_dir/download_file_types.pb"
+  output_dir = target_gen_dir
+  output_basename = "download_file_types.pb"
   python_path_root = "$root_build_dir/pyproto"
   python_path_safe_browsing = "$python_path_root/chrome/common/safe_browsing"
 
@@ -32,18 +35,16 @@
   }
 
   inputs = [
-    script,
     input_filename,
   ]
 
-  # This script requires the generated python proto code
   deps = [
     "//chrome/common/safe_browsing:proto",
     "//third_party/protobuf:py_proto",
   ]
 
   outputs = [
-    output_filename,
+    "$output_dir/$output_basename",
   ]
 
   args = [
@@ -52,8 +53,51 @@
     target_arch,
     "-i",
     rebase_path(input_filename, root_build_dir),
+    "-d",
+    rebase_path(output_dir, root_build_dir),
     "-o",
-    rebase_path(output_filename, root_build_dir),
+    output_basename,
+    "-p",
+    rebase_path(python_path_root, root_build_dir),
+    "-p",
+    rebase_path(python_path_safe_browsing, root_build_dir),
+  ]
+}
+
+# Generate the binary proto for ALL platforms.  This is only run manually
+# when pushing the files to GCS for the component-updater to pick up.
+action("make_all_file_types_protobuf") {
+  script = "gen_file_type_proto.py"
+
+  input_filename = "download_file_types.asciipb"
+  output_dir = "$target_gen_dir/all"
+  output_basename = "download_file_types.pb"
+  python_path_root = "$root_build_dir/pyproto"
+  python_path_safe_browsing = "$python_path_root/chrome/common/safe_browsing"
+
+  inputs = [
+    input_filename,
+  ]
+
+  deps = [
+    "//chrome/common/safe_browsing:proto",
+    "//third_party/protobuf:py_proto",
+  ]
+
+  # A directory, since we can't derive the actual file names here.
+  outputs = [
+    output_dir,
+  ]
+
+  args = [
+    "-w",
+    "-a",
+    "-i",
+    rebase_path(input_filename, root_build_dir),
+    "-d",
+    rebase_path(output_dir, root_build_dir),
+    "-o",
+    output_basename,
     "-p",
     rebase_path(python_path_root, root_build_dir),
     "-p",
diff --git a/chrome/browser/resources/safe_browsing/README.md b/chrome/browser/resources/safe_browsing/README.md
index 503b016..61f2096 100644
--- a/chrome/browser/resources/safe_browsing/README.md
+++ b/chrome/browser/resources/safe_browsing/README.md
@@ -3,16 +3,26 @@
 This describes how to adjust file-type download behavior in
 Chrome including interactions with Safe Browsing. The metadata described
 here, and stored in `download_file_types.asciipb`, will be both baked into
-Chrome released and pushable to Chrome between releases. http://crbug.com/596555
+Chrome released and pushable to Chrome between releases (via
+`FileTypePolicies` class).  http://crbug.com/596555
 
 Rendered version of this file: https://chromium.googlesource.com/chromium/src/+/master/chrome/browser/resources/safe_browsing/README.md
 
 
-## Procedure for adding a new type
-  * Edit `download_file_types.asciipb`, edit `download_stats.cc` (necessary
-    until it gets replaced), and update `histograms.xml`
-  * Get it reviewed, submit.
-  * Push via component update (PROCEDURE TBD)
+## Procedure for adding/modifying file type(s)
+  * **Edit** `download_file_types.asciipb` and update `histograms.xml`
+  * Get it reviewed, **submit.**
+  * **Push** it to all users via component update:
+    * Wait 1-3 day for this to run on Canary to verify it doesn't crash Chrome.
+    * In a synced checkout, generate protos for all platforms:
+        * % `ninja -C out-gn/Debug
+         chrome/browser/resources/safe_browsing:make_all_file_types_protobuf`
+    * That will instruct you to run another command to push the files to GCS.
+      You must a member of chrome-file-type-policies-pushers@google.com to have
+      access to the GCS bucket.
+    * The Component Updater system will notice those files and push them to
+      users withing ~6 hours. If not, contact `waffles@.`
+
 
 ## Guidelines for a DownloadFileType entry:
 See `download_file_types.proto` for all fields.
diff --git a/chrome/browser/resources/safe_browsing/gen_file_type_proto.py b/chrome/browser/resources/safe_browsing/gen_file_type_proto.py
index 1e55f9a1..63ed686 100755
--- a/chrome/browser/resources/safe_browsing/gen_file_type_proto.py
+++ b/chrome/browser/resources/safe_browsing/gen_file_type_proto.py
@@ -11,9 +11,11 @@
 """
 
 import optparse
+import os
 import re
 import subprocess
 import sys
+import traceback
 
 
 def ImportProtoModules(paths):
@@ -93,6 +95,7 @@
 
 def FilterPbForPlatform(full_pb, platform_type):
   """ Return a filtered protobuf for this platform_type """
+  assert type(platform_type) is int, "Bad platform_type type"
 
   new_pb = config_pb2.DownloadFileTypeConfig();
   new_pb.CopyFrom(full_pb)
@@ -127,25 +130,59 @@
   return new_pb
 
 
-def GeneratBinaryProtos(opts):
-  # Read the ASCII
-  ifile = open(opts.infile, 'r')
-  ascii_pb_str = ifile.read()
-  ifile.close()
-
-  # Parse it into a structure PB
-  pb = config_pb2.DownloadFileTypeConfig()
-  text_format.Merge(ascii_pb_str, pb)
-
-  ValidatePb(pb);
-  platform_type = PlatformTypes()[opts.type]
-  filtered_pb = FilterPbForPlatform(pb, platform_type);
+def FilterForPlatformAndWrite(full_pb, platform_type, outfile):
+  """ Filter and write out a file for this platform """
+  # Filter it
+  filtered_pb = FilterPbForPlatform(full_pb, platform_type);
 
   # Serialize it
   binary_pb_str = filtered_pb.SerializeToString()
 
   # Write it to disk
-  open(opts.outfile, 'wb').write(binary_pb_str)
+  open(outfile, 'wb').write(binary_pb_str)
+
+
+def MakeSubDirs(outfile):
+  """ Make the subdirectories needed to create file |outfile| """
+  dirname = os.path.dirname(outfile)
+  if not os.path.exists(dirname):
+    os.makedirs(dirname)
+
+
+def GenerateBinaryProtos(opts):
+  """ Read the ASCII proto and generate one or more binary protos. """
+  # Read the ASCII
+  ifile = open(opts.infile, 'r')
+  ascii_pb_str = ifile.read()
+  ifile.close()
+
+  # Parse it into a structured PB
+  full_pb = config_pb2.DownloadFileTypeConfig()
+  text_format.Merge(ascii_pb_str, full_pb)
+
+  ValidatePb(full_pb);
+
+  if opts.type is not None:
+    # Just one platform type
+    platform_enum = PlatformTypes()[opts.type]
+    outfile = os.path.join(opts.outdir, opts.outbasename)
+    FilterForPlatformAndWrite(full_pb, platform_enum, outfile)
+  else:
+    # Make a separate file for each platform
+    for platform_type, platform_enum in PlatformTypes().iteritems():
+      # e.g. .../all/77/chromeos/download_file_types.pb
+      outfile = os.path.join(opts.outdir,
+                             str(full_pb.version_id),
+                             platform_type,
+                             opts.outbasename)
+      MakeSubDirs(outfile)
+      FilterForPlatformAndWrite(full_pb, platform_enum, outfile)
+
+    print "\n\nTo push these files, run the following:"
+    print ("python " +
+           "chrome/browser/resources/safe_browsing/push_file_type_proto.py " +
+           "-d " + os.path.abspath(opts.outdir))
+    print "\n\n"
 
 
 def main():
@@ -155,44 +192,59 @@
                      help='Wrap this script in another python '
                      'execution to disable site-packages.  This is a '
                      'fix for http://crbug.com/605592')
+
+  parser.add_option('-a', '--all', action="store_true", default=False,
+                     help='Write a separate file for every platform. '
+                    'Outfile must have a %d for version and %s for platform.')
   parser.add_option('-t', '--type',
                     help='The platform type. One of android, chromeos, ' +
                     'linux, mac, win')
   parser.add_option('-i', '--infile',
                     help='The ASCII DownloadFileType-proto file to read.')
-  parser.add_option('-o', '--outfile',
-                    help='The binary file to write to.')
+  parser.add_option('-d', '--outdir',
+                    help='Directory underwhich binary file(s) will be written')
+  parser.add_option('-o', '--outbasename',
+                    help='Basename of the binary file to write to.')
   parser.add_option('-p', '--path', action="append",
                     help='Repeat this as needed.  Directory(s) containing ' +
                     'the download_file_types_pb2.py and ' +
                     'google.protobuf.text_format modules')
   (opts, args) = parser.parse_args()
-  if opts.infile is None or opts.outfile is None:
+  if opts.infile is None or opts.outdir is None or opts.outbasename is None:
     parser.print_help()
     return 1
 
   if opts.wrap:
     # Run this script again with different args to the interpreter.
     command = [sys.executable, '-S', '-s', sys.argv[0]]
-    command += ['-t', opts.type]
+    if opts.type is not None:
+      command += ['-t', opts.type]
+    if opts.all:
+      command += ['-a']
     command += ['-i', opts.infile]
-    command += ['-o', opts.outfile]
+    command += ['-d', opts.outdir]
+    command += ['-o', opts.outbasename]
     for path in opts.path:
       command += ['-p', path]
     sys.exit(subprocess.call(command))
 
   ImportProtoModules(opts.path)
 
-  if (opts.type not in PlatformTypes()):
+  if (not opts.all and opts.type not in PlatformTypes()):
     print "ERROR: Unknown platform type '%s'" % opts.type
     parser.print_help()
     return 1
 
+  if (bool(opts.all) == bool(opts.type)):
+    print "ERROR: Need exactly one of --type or --all"
+    parser.print_help()
+    return 1
+
   try:
-    GeneratBinaryProtos(opts)
+    GenerateBinaryProtos(opts)
   except Exception as e:
-    print "ERROR: Failed to render binary version of %s:\n  %s\n" % (
-        opts.infile, str(e))
+    print "ERROR: Failed to render binary version of %s:\n  %s\n%s" % (
+        opts.infile, str(e), traceback.format_exc())
     return 1
 
 
diff --git a/chrome/browser/resources/safe_browsing/push_file_type_proto.py b/chrome/browser/resources/safe_browsing/push_file_type_proto.py
new file mode 100755
index 0000000..0256ee8
--- /dev/null
+++ b/chrome/browser/resources/safe_browsing/push_file_type_proto.py
@@ -0,0 +1,65 @@
+#!/usr/bin/python
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# Push the {vers}/{plaform}/download_file_types.pb files to GCS so
+# that the component update system will pick them up and push them
+# to users.  See README.md before running this.
+
+import optparse
+import os
+import subprocess
+import sys
+
+
+DEST_BUCKET = 'gs://chrome-component-file-type-policies'
+
+
+def main():
+  parser = optparse.OptionParser()
+  parser.add_option('-d', '--dir',
+                    help='The directory containing '
+                    '{vers}/{platform}/download_file_types.pb files.')
+
+  (opts, args) = parser.parse_args()
+  if opts.dir is None:
+    parser.print_help()
+    return 1
+
+  os.chdir(opts.dir)
+
+  # Sanity check that we're in the right place
+  assert opts.dir.endswith('/all'), '--dir should end with /all'
+  dirs = os.listdir('.')
+  assert (len(dirs) == 1 and dirs[0].isdigit()), (
+      'Should be exactly one version directory. Please delete the contents '
+      'of the target dir and regenerate the protos.')
+
+  # Push the files with their directories, in the form
+  #   {vers}/{platform}/download_file_types.pb
+  # Don't overwrite existing files, incase we forgot to increment the
+  # version.
+  vers_dir = dirs[0]
+  command = ['gsutil', 'cp', '-Rn', vers_dir, DEST_BUCKET]
+
+  print 'Going to run the following command'
+  print '   ', ' '.join(command)
+  print '\nIn directory'
+  print '   ', opts.dir
+  print '\nWhich should push the following files'
+  expected_files = [os.path.join(dp, f) for dp, dn, fn in
+                    os.walk(vers_dir) for f in fn]
+  for f in expected_files:
+    print '   ', f
+
+  shall = raw_input('\nAre you sure (y/N) ').lower() == 'y'
+  if not shall:
+    print 'aborting'
+    return 1
+  return subprocess.call(command)
+
+
+if __name__ == '__main__':
+  sys.exit(main())
+