_text_format.py 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. # Copyright (c) 2025-2026 Buf Technologies, Inc.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. from __future__ import annotations
  15. import re
  16. from protobuf._descriptors import DescEnum, ScalarType
  17. from protobuf._typing import assert_never
  18. def parse_text_format_enum_value(desc: DescEnum, value: str) -> int:
  19. """Parse an enum value from the Protobuf text format."""
  20. value_desc = next((v for v in desc.values if v.name == value), None)
  21. if value_desc is None:
  22. msg = f"cannot parse {desc} default value: {value}"
  23. raise ValueError(msg)
  24. return value_desc.number
  25. def parse_text_format_scalar_value( # noqa: RET503
  26. scalar: ScalarType, value: str
  27. ) -> int | float | bytes | str | bool:
  28. """Parse a scalar value from the Protobuf text format."""
  29. match scalar:
  30. case ScalarType.BOOL:
  31. return value == "true"
  32. case (
  33. ScalarType.INT32
  34. | ScalarType.UINT32
  35. | ScalarType.INT64
  36. | ScalarType.UINT64
  37. | ScalarType.SINT32
  38. | ScalarType.SINT64
  39. | ScalarType.FIXED32
  40. | ScalarType.FIXED64
  41. | ScalarType.SFIXED32
  42. | ScalarType.SFIXED64
  43. ):
  44. return int(value)
  45. case ScalarType.FLOAT | ScalarType.DOUBLE:
  46. return float(value) # Convers inf, -inf, and nan
  47. case ScalarType.STRING:
  48. return value
  49. case ScalarType.BYTES:
  50. return _c_unescape(value)
  51. case _:
  52. assert_never(scalar)
  53. _CUNESCAPE_HEX = re.compile(r"(\\+)x([0-9a-fA-F])(?![0-9a-fA-F])")
  54. _CUNESCAPE_OCTAL = re.compile(r"(\\+)([0-7]{1,3})(?![0-7])")
  55. # Copied from https://github.com/protocolbuffers/protobuf/blob/eedd2068760c946996f3095286492afc0460695a/python/google/protobuf/text_encoding.py#L77
  56. def _c_unescape(text: str) -> bytes:
  57. """Unescape a text string with C-style escape sequences to UTF-8 bytes.
  58. Args:
  59. text: The data to parse in a str.
  60. Returns:
  61. A byte string.
  62. """
  63. def replace_hex(m: re.Match[str]) -> str:
  64. # Only replace the match if the number of leading back slashes is odd. i.e.
  65. # the slash itself is not escaped.
  66. if len(m.group(1)) & 1:
  67. return m.group(1) + "x0" + m.group(2)
  68. return m.group(0)
  69. def replace_octal(m: re.Match[str]) -> str:
  70. if len(m.group(1)) & 1:
  71. return f"{m.group(1)}x{int(m.group(2), 8):02x}"
  72. return m.group(0)
  73. # This is required because the 'string_escape' encoding doesn't
  74. # allow single-digit hex escapes (like '\xf').
  75. result = _CUNESCAPE_HEX.sub(replace_hex, text)
  76. # Pre-convert octal escapes to hex to workaround issue on PyPy with octal escapes
  77. result = _CUNESCAPE_OCTAL.sub(replace_octal, result)
  78. # Replaces Unicode escape sequences with their character equivalents.
  79. result = result.encode("raw_unicode_escape").decode("raw_unicode_escape")
  80. # Encode Unicode characters as UTF-8, then decode to Latin-1 escaping
  81. # unprintable characters.
  82. result = result.encode("utf-8").decode("unicode_escape")
  83. # Convert Latin-1 text back to a byte string (latin-1 codec also works here).
  84. return result.encode("latin-1")