idn_test_case_generator.py 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. #!/usr/bin/env python3
  2. # Copyright 2017 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. """Utilities for generating IDN test cases.
  6. Either use the command-line interface (see --help) or directly call make_case
  7. from Python shell (see make_case documentation).
  8. """
  9. import argparse
  10. import codecs
  11. import doctest
  12. import sys
  13. def str_to_c_string(string):
  14. """Converts a Python bytes to a C++ string literal.
  15. >>> str_to_c_string(b'abc\x8c')
  16. '"abc\\\\x8c"'
  17. """
  18. return repr(string).replace("'", '"').removeprefix('b')
  19. def unicode_to_c_ustring(string):
  20. """Converts a Python unicode string to a C++ u16-string literal.
  21. >>> unicode_to_c_ustring(u'b\u00fccher.de')
  22. 'u"b\\\\u00fccher.de"'
  23. """
  24. result = ['u"']
  25. for c in string:
  26. if (ord(c) > 0xffff):
  27. escaped = '\\U%08x' % ord(c)
  28. elif (ord(c) > 0x7f):
  29. escaped = '\\u%04x' % ord(c)
  30. else:
  31. escaped = c
  32. result.append(escaped)
  33. result.append('"')
  34. return ''.join(result)
  35. def make_case(unicode_domain, unicode_allowed=True, case_name=None):
  36. """Generates a C++ test case for an IDN domain test.
  37. This is designed specifically for the IDNTestCase struct in the file
  38. components/url_formatter/url_formatter_unittest.cc. It generates a row of
  39. the idn_cases array, specifying a test for a particular domain.
  40. |unicode_domain| is a Unicode string of the domain (NOT IDNA-encoded).
  41. |unicode_allowed| specifies whether the test case should expect the domain
  42. to be displayed in Unicode form (kSafe) or in IDNA/Punycode ASCII encoding
  43. (kUnsafe). |case_name| is just for the comment.
  44. This function will automatically convert the domain to its IDNA format, and
  45. prepare the test case in C++ syntax.
  46. >>> make_case(u'\u5317\u4eac\u5927\u5b78.cn', True, 'Hanzi (Chinese)')
  47. // Hanzi (Chinese)
  48. {"xn--1lq90ic7f1rc.cn", u"\\u5317\\u4eac\\u5927\\u5b78.cn", kSafe},
  49. >>> make_case(u'b\u00fccher.de', True)
  50. {"xn--bcher-kva.de", u"b\\u00fccher.de", kSafe},
  51. This will also apply normalization to the Unicode domain, as required by the
  52. IDNA algorithm. This example shows U+210F normalized to U+0127 (this
  53. generates the exact same test case as u'\u0127ello'):
  54. >>> make_case(u'\u210fello', True)
  55. {"xn--ello-4xa", u"\\u0127ello", kSafe},
  56. """
  57. idna_input = codecs.encode(unicode_domain, 'idna')
  58. # Round-trip to ensure normalization.
  59. unicode_output = codecs.decode(idna_input, 'idna')
  60. if case_name:
  61. print(' // %s' % case_name)
  62. print(' {%s, %s, %s},' %
  63. (str_to_c_string(idna_input), unicode_to_c_ustring(unicode_output),
  64. 'kSafe' if unicode_allowed else 'kUnsafe'))
  65. def main(args=None):
  66. if args is None:
  67. args = sys.argv[1:]
  68. parser = argparse.ArgumentParser(description='Generate an IDN test case.')
  69. parser.add_argument('domain',
  70. metavar='DOMAIN',
  71. nargs='?',
  72. help='the Unicode domain (not encoded)')
  73. parser.add_argument('--name',
  74. metavar='NAME',
  75. help='the name of the test case')
  76. parser.add_argument('--no-unicode',
  77. action='store_false',
  78. dest='unicode_allowed',
  79. default=True,
  80. help='expect the domain to be Punycoded')
  81. parser.add_argument('--test',
  82. action='store_true',
  83. dest='run_tests',
  84. help='run unit tests')
  85. args = parser.parse_args(args)
  86. if args.run_tests:
  87. import doctest
  88. doctest.testmod()
  89. return
  90. if not args.domain:
  91. parser.error('Required argument: DOMAIN')
  92. if '://' in args.domain:
  93. parser.error('A URL must not be passed as the domain argument')
  94. make_case(args.domain,
  95. unicode_allowed=args.unicode_allowed,
  96. case_name=args.name)
  97. if __name__ == '__main__':
  98. sys.exit(main())