chromium/printing/backend/tools/code_generator.py

#!/usr/bin/env python
# Copyright 2019 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
'''python %(prog)s [options]
Generate mapping from IPP attribute name to appropriate handler based on its
type as described in IPP registration files.'''

import argparse
import csv
import re

# Skip attributes that are already implemented in print preview plus job-priority (b/172208667).
NOOP_ATTRS = [
    'copies',
    'job-hold-until',
    'job-copies',
    'job-password',
    'job-password-encryption',
    'job-priority',
    'media',
    'media-col',
    'multiple-document-handling',
    'number-up',
    'orientation-requested',
    'page-ranges',
    'presentation-direction-number-up',
    'print-color-mode',
    'print-scaling',
    'printer-resolution',
    'sheet-collate',
    'sides',
]

# RFC 8011 (5.1.4) requires keywords to start with a letter. There's however at
# least one keyword that starts with a digit.
KEYWORD_PATTERN = re.compile(r'^[a-z1-9][a-z0-9\._-]*$')

HANDLER_HEADER = """// DO NOT MODIFY
// Generated by printing/backend/tools/code_generator.py

#include "printing/backend/ipp_handler_map.h"

#include <string_view>

#include "base/functional/bind.h"
#include "printing/backend/ipp_handlers.h"

namespace printing {

HandlerMap GenerateHandlers() {
  HandlerMap result;
"""

HANDLER_FOOTER = """  return result;
}

}  // namespace printing
"""

L10N_HEADER = """// DO NOT MODIFY
// Generated by printing/backend/tools/code_generator.py

#include "chrome/common/printing/ipp_l10n.h"

#include "base/no_destructor.h"
#include "components/strings/grit/components_strings.h"

const std::map<std::string_view, int>& CapabilityLocalizationMap() {
  static const base::NoDestructor<std::map<std::string_view, int>> l10n_map({
"""

L10N_FOOTER = """  });
  return *l10n_map;
}
"""


def get_handler(syntax, name):
  if syntax.startswith('1setOf'):
    handler = get_handler(syntax[6:].strip(), name)
    if handler == 'EnumHandler':
      # 3 is 'none' value for finishings.
      # IPP enums always use positive numbers so we can use 0 in other cases.
      default = 3 if name.endswith('finishings') else 0
      return 'MultivalueEnumHandler, %d' % default

    # TODO(crbug.com/964919): Add other multivalue handlers.
    return ''

  if syntax == 'collection':
    # TODO(crbug.com/964919): Add collection handler.
    return ''

  if syntax.startswith('type1') or syntax.startswith('type2'):
    # ignore prefix
    return get_handler(syntax[5:].strip(), name)

  if syntax.startswith('keyword'):
    return 'KeywordHandler'

  if syntax.startswith('enum'):
    return 'EnumHandler'

  if syntax == 'boolean':
    return 'BooleanHandler'

  if syntax.startswith('integer'):
    # TODO(crbug.com/964919): Add integer handler.
    return 'NumberHandler'

  if syntax.startswith('name') or syntax.startswith('text'):
    return 'TextHandler'

  return ''


# Remove annotations like '(obsolete)', '(deprecated)' etc.
def remove_annotation(keyword):
  parenthesis = keyword.find('(')
  return keyword if parenthesis == -1 else keyword[:parenthesis].strip()


SPECIAL_CHARS = re.compile(r'[-/\.]')


def add_l10n(l10n_file, ipp_id, grit_id=None):
  if not grit_id:
    grit_id = ipp_id

  l10n_file.write('      {"%s", IDS_PRINT_%s},\n' %
                  (ipp_id, SPECIAL_CHARS.sub('_', grit_id.upper())))


def main():
  parser = argparse.ArgumentParser(usage=__doc__)
  parser.add_argument(
      '-a',
      '--attributes-file',
      dest='attributes_file',
      help='path to ipp-registrations-2.csv input file',
      metavar='FILE',
      required=True)
  parser.add_argument(
      '-k',
      '--keyword-values-file',
      dest='keyword_values_file',
      help='path to ipp-registrations-4.csv input file',
      metavar='FILE',
      required=True)
  parser.add_argument(
      '-e',
      '--enum-values-file',
      dest='enum_values_file',
      help='path to ipp-registrations-6.csv input file',
      metavar='FILE',
      required=True)
  parser.add_argument(
      '-i',
      '--ipp-handler-map',
      dest='ipp_handler_map',
      help='path to ipp_handler_map.cc output file',
      metavar='FILE')
  parser.add_argument(
      '-l',
      '--localization-map',
      dest='localization_map',
      help='path to ipp_l10n.cc output file',
      metavar='FILE')
  args = parser.parse_args()

  if not (args.ipp_handler_map or args.localization_map):
    parser.error('No output file selected')

  handlers = []
  supported_items = set()
  with open(args.attributes_file, 'r') as attr_file:
    attr_reader = csv.reader(attr_file)

    for attr in attr_reader:
      # Filter out by attribute group.
      if attr[0] != 'Job Template':
        continue

      # Skip sub-attributes.
      if attr[2] != '':
        continue

      attr_name = remove_annotation(attr[1])
      # Skip duplicates
      if attr_name in supported_items:
        continue

      if not KEYWORD_PATTERN.match(attr_name):
        print('Warning: attribute name %s is invalid' % attr_name)
        continue

      syntax = attr[4]
      handler = 'NoOpHandler'
      if attr_name not in NOOP_ATTRS:
        handler = get_handler(syntax.strip(), attr_name)

      if handler == '':
        continue

      handlers.append((attr_name, handler))
      if handler != 'NoOpHandler':
        supported_items.add(attr_name)

  if args.ipp_handler_map:
    handler_file = open(args.ipp_handler_map, 'w')
    handler_file.write(HANDLER_HEADER)

    for (attr_name, handler) in handlers:
      handler_file.write('  result.emplace("%s", base::BindRepeating(&%s));\n'
                         % (attr_name, handler))

    handler_file.write(HANDLER_FOOTER)
    handler_file.close()

  if args.localization_map:
    l10n_file = open(args.localization_map, 'w')
    l10n_file.write(L10N_HEADER)

    for (attr_name, handler) in handlers:
      if handler != 'NoOpHandler':
        if args.localization_map and not handler.startswith('Multivalue'):
          add_l10n(l10n_file, attr_name)

    # media-source and media-type are only handled for localization because
    # they're inside of the media-col collection and we don't handle
    # collections otherwise.
    supported_items.add("media-source")
    add_l10n(l10n_file, "media-source")
    supported_items.add("media-type")
    add_l10n(l10n_file, "media-type")

    with open(args.keyword_values_file, 'r') as keyword_file:
      keyword_reader = csv.reader(keyword_file)
      for keyword_item in keyword_reader:
        attr_name = keyword_item[0]
        if attr_name in supported_items:
          keyword_value = remove_annotation(keyword_item[1])
          # TODO(crbug.com/964919): Also handle some plain English cases.
          if KEYWORD_PATTERN.match(keyword_value):
            l10n_key = '%s/%s' % (attr_name, keyword_value)
            # Skip duplicates.
            if l10n_key not in supported_items:
              supported_items.add(l10n_key)
              add_l10n(l10n_file, l10n_key)

    with open(args.enum_values_file, 'r') as enum_file:
      enum_reader = csv.reader(enum_file)
      for enum_item in enum_reader:
        attr_name = enum_item[0]
        if attr_name in supported_items:
          enum_value = enum_item[1]
          try:
            int(enum_value)
            l10n_key = attr_name + '/' + enum_value
            # Skip duplicates and finishings 'none' value.
            if l10n_key not in supported_items and l10n_key != 'finishings/3':
              supported_items.add(l10n_key)
              add_l10n(l10n_file, l10n_key,
                       attr_name + '_' + remove_annotation(enum_item[2]))
          except ValueError:
            # TODO(crbug.com/964919): Handle some plain English cases.
            pass

    l10n_file.write(L10N_FOOTER)
    l10n_file.close()


if __name__ == '__main__':
  main()