protostar 0.1.0.beta1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +60 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +647 -0
  5. data/THIRD_PARTY_NOTICES.md +61 -0
  6. data/exe/protoc-gen-protostar +7 -0
  7. data/exe/protostar +7 -0
  8. data/lib/protostar/builder/generator.rb +71 -0
  9. data/lib/protostar/builder/sink.rb +35 -0
  10. data/lib/protostar/builder.rb +965 -0
  11. data/lib/protostar/cli.rb +267 -0
  12. data/lib/protostar/codec/decode_generator.rb +199 -0
  13. data/lib/protostar/codec/decoder.rb +296 -0
  14. data/lib/protostar/codec/encode_generator.rb +224 -0
  15. data/lib/protostar/codec/encoder.rb +121 -0
  16. data/lib/protostar/codec/sizer.rb +91 -0
  17. data/lib/protostar/codec.rb +15 -0
  18. data/lib/protostar/compiler/ast.rb +193 -0
  19. data/lib/protostar/compiler/emitters/descriptor_set.rb +57 -0
  20. data/lib/protostar/compiler/emitters/google_ruby.rb +103 -0
  21. data/lib/protostar/compiler/emitters/grpc.rb +175 -0
  22. data/lib/protostar/compiler/emitters/protostar_rbs.rb +252 -0
  23. data/lib/protostar/compiler/emitters/rbs.rb +394 -0
  24. data/lib/protostar/compiler/emitters/ruby.rb +503 -0
  25. data/lib/protostar/compiler/emitters.rb +268 -0
  26. data/lib/protostar/compiler/include/LICENSE +32 -0
  27. data/lib/protostar/compiler/include/google/protobuf/any.proto +106 -0
  28. data/lib/protostar/compiler/include/google/protobuf/api.proto +229 -0
  29. data/lib/protostar/compiler/include/google/protobuf/compiler/plugin.proto +180 -0
  30. data/lib/protostar/compiler/include/google/protobuf/cpp_features.proto +87 -0
  31. data/lib/protostar/compiler/include/google/protobuf/descriptor.proto +1532 -0
  32. data/lib/protostar/compiler/include/google/protobuf/duration.proto +115 -0
  33. data/lib/protostar/compiler/include/google/protobuf/empty.proto +51 -0
  34. data/lib/protostar/compiler/include/google/protobuf/field_mask.proto +243 -0
  35. data/lib/protostar/compiler/include/google/protobuf/java_features.proto +132 -0
  36. data/lib/protostar/compiler/include/google/protobuf/source_context.proto +48 -0
  37. data/lib/protostar/compiler/include/google/protobuf/struct.proto +111 -0
  38. data/lib/protostar/compiler/include/google/protobuf/timestamp.proto +145 -0
  39. data/lib/protostar/compiler/include/google/protobuf/type.proto +217 -0
  40. data/lib/protostar/compiler/include/google/protobuf/wrappers.proto +157 -0
  41. data/lib/protostar/compiler/lexer.rb +659 -0
  42. data/lib/protostar/compiler/linker.rb +894 -0
  43. data/lib/protostar/compiler/option_interpreter.rb +363 -0
  44. data/lib/protostar/compiler/parser.rb +1696 -0
  45. data/lib/protostar/compiler/retention.rb +104 -0
  46. data/lib/protostar/compiler/run.rb +184 -0
  47. data/lib/protostar/compiler/source_info.rb +161 -0
  48. data/lib/protostar/compiler/symbol_table.rb +223 -0
  49. data/lib/protostar/compiler/text_format.rb +340 -0
  50. data/lib/protostar/compiler/validator.rb +944 -0
  51. data/lib/protostar/compiler.rb +304 -0
  52. data/lib/protostar/diagnostics/codes.rb +1307 -0
  53. data/lib/protostar/diagnostics/formats.rb +64 -0
  54. data/lib/protostar/diagnostics/renderer.rb +240 -0
  55. data/lib/protostar/diagnostics/source.rb +153 -0
  56. data/lib/protostar/diagnostics.rb +117 -0
  57. data/lib/protostar/dsl.rb +367 -0
  58. data/lib/protostar/enum.rb +70 -0
  59. data/lib/protostar/errors.rb +23 -0
  60. data/lib/protostar/google/protobuf/any_pb.rb +90 -0
  61. data/lib/protostar/google/protobuf/api_pb.rb +199 -0
  62. data/lib/protostar/google/protobuf/compiler/plugin_pb.rb +168 -0
  63. data/lib/protostar/google/protobuf/descriptor_pb.rb +1239 -0
  64. data/lib/protostar/google/protobuf/duration_pb.rb +99 -0
  65. data/lib/protostar/google/protobuf/empty_pb.rb +36 -0
  66. data/lib/protostar/google/protobuf/field_mask_pb.rb +228 -0
  67. data/lib/protostar/google/protobuf/source_context_pb.rb +33 -0
  68. data/lib/protostar/google/protobuf/struct_pb.rb +91 -0
  69. data/lib/protostar/google/protobuf/timestamp_pb.rb +129 -0
  70. data/lib/protostar/google/protobuf/type_pb.rb +202 -0
  71. data/lib/protostar/google/protobuf/wrappers_pb.rb +125 -0
  72. data/lib/protostar/google_protobuf_shim.rb +24 -0
  73. data/lib/protostar/json/encoder.rb +340 -0
  74. data/lib/protostar/json/errors.rb +34 -0
  75. data/lib/protostar/json/parser.rb +437 -0
  76. data/lib/protostar/json/path.rb +39 -0
  77. data/lib/protostar/json.rb +95 -0
  78. data/lib/protostar/lint/command.rb +197 -0
  79. data/lib/protostar/lint/config.rb +98 -0
  80. data/lib/protostar/lint/context.rb +126 -0
  81. data/lib/protostar/lint/directives.rb +56 -0
  82. data/lib/protostar/lint/fixer.rb +165 -0
  83. data/lib/protostar/lint/naming.rb +54 -0
  84. data/lib/protostar/lint/rule.rb +120 -0
  85. data/lib/protostar/lint/rules/comment_enum.rb +31 -0
  86. data/lib/protostar/lint/rules/comment_enum_value.rb +32 -0
  87. data/lib/protostar/lint/rules/comment_field.rb +33 -0
  88. data/lib/protostar/lint/rules/comment_message.rb +31 -0
  89. data/lib/protostar/lint/rules/comment_rpc.rb +31 -0
  90. data/lib/protostar/lint/rules/comment_service.rb +31 -0
  91. data/lib/protostar/lint/rules/edition_recommended.rb +31 -0
  92. data/lib/protostar/lint/rules/enum_first_value_zero.rb +34 -0
  93. data/lib/protostar/lint/rules/enum_pascal_case.rb +25 -0
  94. data/lib/protostar/lint/rules/enum_value_prefix.rb +38 -0
  95. data/lib/protostar/lint/rules/enum_value_upper_snake_case.rb +28 -0
  96. data/lib/protostar/lint/rules/enum_zero_value_suffix.rb +41 -0
  97. data/lib/protostar/lint/rules/field_lower_snake_case.rb +34 -0
  98. data/lib/protostar/lint/rules/field_not_required.rb +39 -0
  99. data/lib/protostar/lint/rules/field_number_reuse_risk.rb +45 -0
  100. data/lib/protostar/lint/rules/field_numbers_hot_range.rb +44 -0
  101. data/lib/protostar/lint/rules/file_lower_snake_case.rb +27 -0
  102. data/lib/protostar/lint/rules/import_no_public.rb +29 -0
  103. data/lib/protostar/lint/rules/import_no_weak.rb +28 -0
  104. data/lib/protostar/lint/rules/import_used.rb +33 -0
  105. data/lib/protostar/lint/rules/message_pascal_case.rb +27 -0
  106. data/lib/protostar/lint/rules/no_deprecated_features.rb +50 -0
  107. data/lib/protostar/lint/rules/no_groups.rb +34 -0
  108. data/lib/protostar/lint/rules/oneof_lower_snake_case.rb +27 -0
  109. data/lib/protostar/lint/rules/package_defined.rb +28 -0
  110. data/lib/protostar/lint/rules/package_directory_match.rb +33 -0
  111. data/lib/protostar/lint/rules/package_lower_snake_case.rb +28 -0
  112. data/lib/protostar/lint/rules/package_same_directory.rb +39 -0
  113. data/lib/protostar/lint/rules/package_version_suffix.rb +31 -0
  114. data/lib/protostar/lint/rules/reserved_names_with_numbers.rb +44 -0
  115. data/lib/protostar/lint/rules/rpc_pascal_case.rb +25 -0
  116. data/lib/protostar/lint/rules/rpc_request_response_unique.rb +40 -0
  117. data/lib/protostar/lint/rules/rpc_request_standard_name.rb +32 -0
  118. data/lib/protostar/lint/rules/rpc_response_standard_name.rb +31 -0
  119. data/lib/protostar/lint/rules/service_pascal_case.rb +25 -0
  120. data/lib/protostar/lint/rules/service_suffix.rb +28 -0
  121. data/lib/protostar/lint/rules/syntax_specified.rb +35 -0
  122. data/lib/protostar/lint/runner.rb +135 -0
  123. data/lib/protostar/lint.rb +122 -0
  124. data/lib/protostar/message/accessors.rb +86 -0
  125. data/lib/protostar/message/coercion.rb +171 -0
  126. data/lib/protostar/message/field_info.rb +145 -0
  127. data/lib/protostar/message/reflection.rb +149 -0
  128. data/lib/protostar/message.rb +572 -0
  129. data/lib/protostar/plugin.rb +94 -0
  130. data/lib/protostar/schema/default_value.rb +113 -0
  131. data/lib/protostar/schema/descriptor.rb +41 -0
  132. data/lib/protostar/schema/draft/copy.rb +88 -0
  133. data/lib/protostar/schema/draft/resolution.rb +177 -0
  134. data/lib/protostar/schema/draft.rb +779 -0
  135. data/lib/protostar/schema/enum.rb +106 -0
  136. data/lib/protostar/schema/extension_range.rb +37 -0
  137. data/lib/protostar/schema/features.rb +153 -0
  138. data/lib/protostar/schema/field.rb +201 -0
  139. data/lib/protostar/schema/file.rb +217 -0
  140. data/lib/protostar/schema/freezer.rb +61 -0
  141. data/lib/protostar/schema/message.rb +104 -0
  142. data/lib/protostar/schema/oneof.rb +38 -0
  143. data/lib/protostar/schema/options.rb +367 -0
  144. data/lib/protostar/schema/pool.rb +179 -0
  145. data/lib/protostar/schema/proto.rb +576 -0
  146. data/lib/protostar/schema/resolver.rb +118 -0
  147. data/lib/protostar/schema/service.rb +89 -0
  148. data/lib/protostar/schema/source_info.rb +53 -0
  149. data/lib/protostar/schema.rb +65 -0
  150. data/lib/protostar/short_names/any.rb +7 -0
  151. data/lib/protostar/short_names/duration.rb +7 -0
  152. data/lib/protostar/short_names/empty.rb +7 -0
  153. data/lib/protostar/short_names/field_mask.rb +7 -0
  154. data/lib/protostar/short_names/struct.rb +9 -0
  155. data/lib/protostar/short_names/timestamp.rb +7 -0
  156. data/lib/protostar/short_names/wrappers.rb +15 -0
  157. data/lib/protostar/text/extensions.rb +114 -0
  158. data/lib/protostar/text/parser.rb +439 -0
  159. data/lib/protostar/text/printer.rb +304 -0
  160. data/lib/protostar/text/tokenizer.rb +116 -0
  161. data/lib/protostar/text.rb +67 -0
  162. data/lib/protostar/types.rb +152 -0
  163. data/lib/protostar/unknown_field.rb +58 -0
  164. data/lib/protostar/version.rb +5 -0
  165. data/lib/protostar/view/generator.rb +298 -0
  166. data/lib/protostar/view/map.rb +194 -0
  167. data/lib/protostar/view/repeated.rb +143 -0
  168. data/lib/protostar/view/scan.rb +183 -0
  169. data/lib/protostar/view.rb +283 -0
  170. data/lib/protostar/well_known/any.rb +52 -0
  171. data/lib/protostar/well_known/duration.rb +53 -0
  172. data/lib/protostar/well_known/list_value.rb +63 -0
  173. data/lib/protostar/well_known/struct.rb +55 -0
  174. data/lib/protostar/well_known/timestamp.rb +64 -0
  175. data/lib/protostar/well_known/value.rb +60 -0
  176. data/lib/protostar/well_known.rb +38 -0
  177. data/lib/protostar/wire/io_source.rb +40 -0
  178. data/lib/protostar/wire/reader.rb +254 -0
  179. data/lib/protostar/wire/writer.rb +201 -0
  180. data/lib/protostar/wire.rb +96 -0
  181. data/lib/protostar.rb +95 -0
  182. data/shims/google-protobuf/lib/google/protobuf/any_pb.rb +17 -0
  183. data/shims/google-protobuf/lib/google/protobuf/api_pb.rb +22 -0
  184. data/shims/google-protobuf/lib/google/protobuf/compiler/plugin_pb.rb +25 -0
  185. data/shims/google-protobuf/lib/google/protobuf/descriptor_pb.rb +72 -0
  186. data/shims/google-protobuf/lib/google/protobuf/descriptor_pool.rb +164 -0
  187. data/shims/google-protobuf/lib/google/protobuf/descriptors.rb +553 -0
  188. data/shims/google-protobuf/lib/google/protobuf/duration_pb.rb +17 -0
  189. data/shims/google-protobuf/lib/google/protobuf/empty_pb.rb +17 -0
  190. data/shims/google-protobuf/lib/google/protobuf/field_mask_pb.rb +17 -0
  191. data/shims/google-protobuf/lib/google/protobuf/internal/convert.rb +180 -0
  192. data/shims/google-protobuf/lib/google/protobuf/internal/extensions.rb +154 -0
  193. data/shims/google-protobuf/lib/google/protobuf/internal/json.rb +104 -0
  194. data/shims/google-protobuf/lib/google/protobuf/internal/well_known.rb +140 -0
  195. data/shims/google-protobuf/lib/google/protobuf/map.rb +216 -0
  196. data/shims/google-protobuf/lib/google/protobuf/message.rb +544 -0
  197. data/shims/google-protobuf/lib/google/protobuf/message_exts.rb +41 -0
  198. data/shims/google-protobuf/lib/google/protobuf/plugin_pb.rb +4 -0
  199. data/shims/google-protobuf/lib/google/protobuf/repeated_field.rb +366 -0
  200. data/shims/google-protobuf/lib/google/protobuf/source_context_pb.rb +17 -0
  201. data/shims/google-protobuf/lib/google/protobuf/struct_pb.rb +20 -0
  202. data/shims/google-protobuf/lib/google/protobuf/timestamp_pb.rb +17 -0
  203. data/shims/google-protobuf/lib/google/protobuf/type_pb.rb +27 -0
  204. data/shims/google-protobuf/lib/google/protobuf/well_known_types.rb +37 -0
  205. data/shims/google-protobuf/lib/google/protobuf/wrappers_pb.rb +25 -0
  206. data/shims/google-protobuf/lib/google/protobuf.rb +107 -0
  207. data/shims/google-protobuf/lib/google/protobuf_ffi.rb +5 -0
  208. data/shims/google-protobuf/lib/google/protobuf_native.rb +5 -0
  209. data/sig/protostar/google/protobuf/any_pb.rbs +77 -0
  210. data/sig/protostar/google/protobuf/api_pb.rbs +242 -0
  211. data/sig/protostar/google/protobuf/compiler/plugin_pb.rbs +264 -0
  212. data/sig/protostar/google/protobuf/descriptor_pb.rbs +2220 -0
  213. data/sig/protostar/google/protobuf/duration_pb.rbs +82 -0
  214. data/sig/protostar/google/protobuf/empty_pb.rbs +18 -0
  215. data/sig/protostar/google/protobuf/field_mask_pb.rbs +170 -0
  216. data/sig/protostar/google/protobuf/source_context_pb.rbs +24 -0
  217. data/sig/protostar/google/protobuf/struct_pb.rbs +135 -0
  218. data/sig/protostar/google/protobuf/timestamp_pb.rbs +102 -0
  219. data/sig/protostar/google/protobuf/type_pb.rbs +304 -0
  220. data/sig/protostar/google/protobuf/wrappers_pb.rbs +169 -0
  221. data/sig/protostar/lint.rbs +216 -0
  222. data/sig/protostar.rbs +326 -0
  223. metadata +366 -0
@@ -0,0 +1,267 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+ require "fileutils"
5
+ require "protostar"
6
+
7
+ module Protostar
8
+ # `protostar compile`, with protoc's flag names and protoc's messages
9
+ #
10
+ # protostar compile -I proto --ruby_out=lib --grpc_out=lib acme/service.proto
11
+ #
12
+ # `compile` is the default command. Without `-I` every input's own directory
13
+ # is an include path, and one input with no `--*_out` prints its native Ruby:
14
+ #
15
+ # protostar proto/acme/person.proto > person_pb.rb
16
+ class CLI
17
+ USAGE = "Usage: protostar [compile] [OPTION] PROTO_FILES\n protostar lint [OPTION] PROTO_FILES_OR_DIRS\n" \
18
+ " protostar --version"
19
+ DEFAULTS = "Without -I, the directory of each PROTO_FILE is an include path and the file is named by its\n" \
20
+ "basename. With one PROTO_FILE and no --*_out, what --ruby_out would write is printed to stdout."
21
+ NO_STDIN = "Reading a .proto from stdin (-) is not supported; pass the file's path."
22
+ Emitters = Compiler::Emitters
23
+ RBS_EDITIONS = "is an editions file, but code generator --rbs_out hasn't been updated to support editions yet. " \
24
+ "Please ask the owner of this code generator to add support or switch back to proto2/proto3.\n\n" \
25
+ "See https://protobuf.dev/editions/overview/ for more information."
26
+ NO_MAPPING = "File does not reside within any path specified using --proto_path (or -I). You must specify a " \
27
+ "--proto_path which encompasses this file. Note that the proto_path must be an exact prefix of the .proto " \
28
+ "file names -- protoc is too dumb to figure out when two paths (e.g. absolute and relative) are equivalent " \
29
+ "(it's harder than you think)."
30
+
31
+ # Raised to stop with a message on stderr and exit status 1
32
+ class Failure < Protostar::Error; end
33
+
34
+ # @param argv [Array<String>]
35
+ # @param out [IO]
36
+ # @param err [IO]
37
+ # @return [Integer] the exit status
38
+ def self.run(argv, out: $stdout, err: $stderr)
39
+ new(out, err).run(argv)
40
+ end
41
+
42
+ # Whether to colour what goes to `io`: a terminal, and NO_COLOR unset
43
+ # @api private
44
+ def self.color?(io)
45
+ io.respond_to?(:tty?) && io.tty? && ENV.fetch("NO_COLOR", "").empty?
46
+ end
47
+
48
+ def initialize(out, err)
49
+ @out = out
50
+ @err = err
51
+ end
52
+
53
+ # @return [Integer]
54
+ def run(argv)
55
+ argv = argv.dup
56
+ case argv.first
57
+ when "--version", "-v"
58
+ @out.puts "protostar #{VERSION}"
59
+ 0
60
+ when "--help", "-h", nil
61
+ @out.puts USAGE, "", DEFAULTS, "", parser({}).summarize
62
+ argv.empty? ? 1 : 0
63
+ when "compile"
64
+ argv.shift
65
+ compile(argv)
66
+ when "lint"
67
+ argv.shift
68
+ Lint::Command.new(@out, @err).run(argv)
69
+ else
70
+ return compile(argv) unless command?(argv.first)
71
+ @err.puts "Unknown command: #{argv.first}", USAGE
72
+ 1
73
+ end
74
+ rescue Failure => e
75
+ @err.write(e.message)
76
+ @err.write("\n") unless e.message.end_with?("\n")
77
+ 1
78
+ end
79
+
80
+ private
81
+
82
+ # A bare word that is not a file is a mistyped command, not a missing input
83
+ def command?(arg)
84
+ !arg.start_with?("-") && !arg.end_with?(".proto") && !::File.exist?(arg)
85
+ end
86
+
87
+ def parser(opts)
88
+ OptionParser.new do |o|
89
+ o.banner = USAGE
90
+ o.on("-I", "--proto_path=PATH", "Directory to search for imports; repeatable, or colon-separated") do |v|
91
+ (opts[:include] ||= []).concat(v.split(":"))
92
+ end
93
+ o.on("--ruby_out=DIR", "Write protostar's native Ruby (*_pb.rb)") { |v| opts[:ruby] = v }
94
+ o.on("--google_out=DIR", "Write protoc's google-protobuf Ruby (*_pb.rb)") { |v| opts[:google] = v }
95
+ o.on("--rbs_out=DIR", "Write RBS signatures (*_pb.rbs): for protostar's classes with --ruby_out, else protoc's") { |v| opts[:rbs] = v }
96
+ o.on("--grpc_out=DIR", "Write gRPC service stubs (*_services_pb.rb)") { |v| opts[:grpc] = v }
97
+ o.on("--descriptor_set_out=FILE", "Write a FileDescriptorSet") { |v| opts[:descriptor_set] = v }
98
+ o.on("--include_imports", "With --descriptor_set_out, include every import") { opts[:include_imports] = true }
99
+ o.on("--include_source_info", "With --descriptor_set_out, keep source info") { opts[:include_source_info] = true }
100
+ o.on("--error_format=FORMAT", "gcc (default), msvs, or rich: rustc-style, with the source and a fix") do |v|
101
+ opts[:error_format] = v
102
+ end
103
+ end
104
+ end
105
+
106
+ def compile(argv)
107
+ opts = {}
108
+ files = begin
109
+ parser(opts).parse(argv)
110
+ rescue OptionParser::ParseError => e
111
+ raise Failure, e.message
112
+ end
113
+ format = opts.fetch(:error_format, "gcc")
114
+ raise Failure, "Unknown error format: #{format}" unless %w[gcc msvs rich].include?(format)
115
+ @format = format.to_sym
116
+ raise Failure, NO_STDIN if files.include?("-")
117
+ raise Failure, "Missing input file." if files.empty?
118
+ outputs = %i[ruby google rbs grpc descriptor_set].select { |k| opts[k] }
119
+ if outputs.empty?
120
+ raise Failure, "--include_imports writes a set of files; add --descriptor_set_out=FILE." if opts[:include_imports]
121
+ raise Failure, "#{files.size} files cannot share stdout; add --ruby_out=DIR to write a _pb.rb for each." if files.size > 1
122
+ end
123
+ include = opts[:include] || default_include(files)
124
+ compiler = Compiler.new(include_paths: include)
125
+ names = files.map { |f| opts[:include] ? virtual_name(f, include, compiler) : default_name(f, include, compiler) }
126
+ compiled = begin
127
+ compiler.compile(names, source_info: true)
128
+ rescue Compiler::Error => e
129
+ lines = e.diagnostics.map { |d| show(d, compiler) }
130
+ lines.concat(import_hints(e.diagnostics, include)) unless opts[:include]
131
+ raise Failure, lines.join("\n")
132
+ end
133
+ compiler.diagnostics.each { |d| @err.puts show(d, compiler) }
134
+ named = names.map { |n| compiled.find { |f| f.name == n } }.uniq
135
+ return print_ruby(named.first) if outputs.empty?
136
+ written = generate(opts, named, compiled, compiler)
137
+ written.each do |path, content|
138
+ FileUtils.mkdir_p(::File.dirname(path))
139
+ ::File.binwrite(path, content)
140
+ end
141
+ 0
142
+ end
143
+
144
+ # protoc's one line, or with --error_format=rich, rustc's several
145
+ def show(diagnostic, compiler)
146
+ return diagnostic.format(@format) unless @format == :rich
147
+ @renderer ||= Diagnostics::Renderer.new(color: CLI.color?(@err),
148
+ source: ->(d) { (path = compiler.resolve(d.file)) && ::File.binread(path) })
149
+ @renderer.render(diagnostic)
150
+ end
151
+
152
+ def print_ruby(file)
153
+ @out.write(Emitters::Ruby.new.file(file))
154
+ 0
155
+ rescue Emitters::Error => e
156
+ raise Failure, "#{file.name}: #{e.message}"
157
+ end
158
+
159
+ # The directories of the inputs on disk, in the order given. Each input is
160
+ # named by its basename then, so two inputs of one name cannot both be
161
+ def default_include(files)
162
+ seen = {}
163
+ files.select { |f| ::File.file?(f) }.map do |f|
164
+ other = seen[::File.basename(f)] ||= f
165
+ unless ::File.expand_path(other) == ::File.expand_path(f)
166
+ raise Failure, "#{other} and #{f} are both #{::File.basename(f)}; pass -I with a directory above both to tell them apart."
167
+ end
168
+ ::File.dirname(f)
169
+ end.uniq { |dir| ::File.expand_path(dir) }
170
+ end
171
+
172
+ def default_name(arg, include, compiler)
173
+ return virtual_name(arg, include, compiler) unless ::File.file?(arg)
174
+ name = ::File.basename(arg)
175
+ found = compiler.resolve(name)
176
+ return name if found == ::File.expand_path(arg)
177
+ raise Failure, "#{arg}: Input is shadowed by #{shorten(found)}, which is next to another input. Pass -I to choose one."
178
+ end
179
+
180
+ # An import not found, but one directory above an input's, was most
181
+ # likely meant relative to that parent
182
+ def import_hints(diagnostics, include)
183
+ missing = diagnostics.select { |d| d.line.nil? && d.message == "File not found." }.map(&:file).uniq
184
+ missing.filter_map do |name|
185
+ parent = include.map { |dir| ::File.dirname(::File.expand_path(dir)) }
186
+ .find { |dir| ::File.file?(::File.join(dir, name)) }
187
+ next unless parent
188
+ shown = shorten(parent)
189
+ "#{name} is in #{shown}/, which is not an include path. Add -I #{shown} to find it there."
190
+ end
191
+ end
192
+
193
+ def shorten(dir)
194
+ cwd = ::Dir.pwd
195
+ return "." if dir == cwd
196
+ dir.delete_prefix("#{cwd}/")
197
+ end
198
+
199
+ # Every output, generated before anything is written, as protoc does
200
+ def generate(opts, named, compiled, compiler)
201
+ written = {}
202
+ %i[ruby google rbs grpc].each do |kind|
203
+ next unless (dir = opts[kind])
204
+ check_dir(dir)
205
+ flag = "--#{kind}_out"
206
+ named.each do |file|
207
+ if kind == :rbs && file.syntax == :editions && !native_rbs?(opts)
208
+ raise Failure, "#{file.name}: #{RBS_EDITIONS}--rbs_out: "
209
+ end
210
+ begin
211
+ emitter(kind, compiler, opts).emit([file]).each { |path, content| written[::File.join(dir, path)] = content }
212
+ rescue Emitters::Error => e
213
+ raise Failure, "#{flag}: #{file.name}: #{e.message}"
214
+ end
215
+ end
216
+ end
217
+ if (path = opts[:descriptor_set])
218
+ set = Emitters::DescriptorSet.new(include_imports: opts[:include_imports],
219
+ include_source_info: opts[:include_source_info], pool: compiler.pool)
220
+ written[path] = set.emit(named, available: compiled)
221
+ end
222
+ written
223
+ end
224
+
225
+ def emitter(kind, compiler, opts)
226
+ case kind
227
+ when :ruby then Emitters::Ruby.new
228
+ when :google then Emitters::GoogleRuby.new(pool: compiler.pool)
229
+ when :rbs then native_rbs?(opts) ? Emitters::ProtostarRbs.new : Emitters::Rbs.new
230
+ when :grpc then Emitters::Grpc.new
231
+ end
232
+ end
233
+
234
+ # --rbs_out describes protostar's classes next to --ruby_out, and
235
+ # google-protobuf's as protoc does otherwise
236
+ def native_rbs?(opts)
237
+ opts[:ruby] && !opts[:google]
238
+ end
239
+
240
+ def check_dir(dir)
241
+ raise Failure, "#{dir}/: No such file or directory" unless ::File.directory?(dir)
242
+ end
243
+
244
+ # protoc's MakeInputsBeProtoPathRelative: a file argument is a path on
245
+ # disk under an include path, or else a name relative to one
246
+ def virtual_name(arg, include, compiler)
247
+ unless ::File.exist?(arg)
248
+ return arg if compiler.resolve(arg)
249
+ raise Failure, "Could not make proto path relative: #{arg}: No such file or directory"
250
+ end
251
+ mapped = disk_to_virtual(arg, include)
252
+ return mapped if mapped
253
+ return arg if compiler.resolve(arg)
254
+ raise Failure, "#{arg}: #{NO_MAPPING}"
255
+ end
256
+
257
+ def disk_to_virtual(arg, include)
258
+ path = Compiler.canonical_path(arg)
259
+ include.each do |dir|
260
+ dir = Compiler.canonical_path(dir)
261
+ return path if dir.empty? && !path.start_with?("/")
262
+ return path.delete_prefix("#{dir}/") if path.start_with?("#{dir}/")
263
+ end
264
+ nil
265
+ end
266
+ end
267
+ end
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Protostar
4
+ module Codec
5
+ # Writes a decoding loop for one message class as Ruby source and evals
6
+ # it into the class as `__decode_fields`. The loop dispatches on the whole
7
+ # tag with one `case` (field number and wire type at once), reads varints
8
+ # of one byte and fixed-width values inline, and keeps the read position
9
+ # in a local. Anything it has no branch for (unknown fields, wire type
10
+ # mismatches, end-group tags, malformed tags) goes to {Decoder.field}.
11
+ #
12
+ # A generic loop over field descriptors spent most of its time in method
13
+ # calls on the field table; this does the same work with a fraction of them.
14
+ # @api private
15
+ module DecodeGenerator
16
+ F = Message::FieldInfo
17
+
18
+ module_function
19
+
20
+ # @param klass [Class]
21
+ # @param fields [Array<Message::FieldInfo>]
22
+ def install(klass, fields)
23
+ klass.class_eval(source(fields), "(protostar decoder #{klass.descriptor.full_name})", 1)
24
+ end
25
+
26
+ def source(fields)
27
+ branches = fields.map { |info| branch(info) }.join
28
+ packed = fields.select { |info| info.packable && VARINT_KINDS.include?(info.kind) }.map { |info| packed_method(info) }
29
+ <<~RUBY
30
+ # frozen_string_literal: true
31
+ #{packed.join}
32
+ def __decode_fields(r, depth, end_group, frozen)
33
+ #{"values = @values" unless fields.empty?}
34
+ buf = r.buffer
35
+ limit = r.limit
36
+ pos = r.pos
37
+ while pos < limit
38
+ at = pos
39
+ if (tag = buf.getbyte(pos)) < 0x80
40
+ pos += 1
41
+ elsif pos + 1 < limit && (b = buf.getbyte(pos + 1)) < 0x80
42
+ tag = (tag & 0x7F) | (b << 7)
43
+ pos += 2
44
+ else
45
+ r.pos = pos
46
+ tag = r.read_tag_varint
47
+ pos = r.pos
48
+ end
49
+ #{branches.empty? ? "if false" : "case tag"}
50
+ #{branches}
51
+ else
52
+ r.pos = pos
53
+ if Protostar::Codec::Decoder.field(self, r, tag, at, depth, end_group, frozen)
54
+ @unknown&.freeze
55
+ return
56
+ end
57
+ limit = r.limit
58
+ pos = r.pos
59
+ end
60
+ end
61
+ r.pos = pos
62
+ Protostar::Codec::Decoder.finish(self, r, end_group)
63
+ end
64
+ RUBY
65
+ end
66
+
67
+ def branch(info)
68
+ i = info.index
69
+ tag = (info.number << 3) | info.wire_type
70
+ out = "when #{tag} # #{info.name}\n"
71
+ out << if info.map
72
+ slow("Protostar::Codec::Decoder.map_entry(self, values, self.class.__fields[#{i}], r, depth, frozen)")
73
+ elsif info.message?
74
+ if info.repeated
75
+ slow("Protostar::Codec::Decoder.repeated_message(values, self.class.__fields[#{i}], r, depth, frozen, at)")
76
+ else
77
+ slow("Protostar::Codec::Decoder.singular_message(values, self.class.__fields[#{i}], r, depth, frozen, at)")
78
+ end
79
+ elsif info.kind == F::ENUM
80
+ enum(info)
81
+ else
82
+ "#{read(info)}#{store(info, "v")}"
83
+ end
84
+ if info.packable
85
+ packed_tag = (info.number << 3) | Wire::LENGTH_DELIMITED
86
+ out << "when #{packed_tag} # #{info.name}, packed\n"
87
+ out << if VARINT_KINDS.include?(info.kind)
88
+ "#{varint("n")}raise Protostar::DecodeError.new(\"length \#{n} runs past the end of the input\", offset: pos) " \
89
+ "if pos + n > limit\npos = __decode_packed#{i}(r, buf, pos, pos + n, limit, (values[#{i}] ||= []))\n"
90
+ else
91
+ slow("Protostar::Codec::Decoder.packed(self, values, self.class.__fields[#{i}], r)")
92
+ end
93
+ end
94
+ out
95
+ end
96
+
97
+ VARINT_KINDS = [F::INT32, F::INT64, F::UINT32, F::UINT64, F::SINT32, F::SINT64, F::BOOL].freeze
98
+
99
+ # A method reading a packed run of varints into `list` in one loop,
100
+ # rather than a Decoder call per element; it returns the position
101
+ # after the run. Kept out of `__decode_fields`, which YJIT runs slower
102
+ # with the loops inline.
103
+ def packed_method(info)
104
+ <<~RUBY
105
+ def __decode_packed#{info.index}(r, buf, pos, stop, limit, list)
106
+ while pos < stop
107
+ #{read(info, "stop")}list << v
108
+ end
109
+ pos
110
+ end
111
+ RUBY
112
+ end
113
+
114
+ # Hands the reader to a Decoder method and picks the position back up
115
+ def slow(call)
116
+ "r.pos = pos\n#{call}\npos = r.pos\n"
117
+ end
118
+
119
+ # Source reading a varint into `var`, inline when it is one or two
120
+ # bytes; `lim` is where its input ends (a packed run's end, say)
121
+ def varint(var, lim = "limit")
122
+ fallback = if lim == "limit"
123
+ "r.pos = pos\n#{var} = r.read_varint\npos = r.pos\n"
124
+ else
125
+ "r.pos = pos\nr.pop_limit(#{lim})\n#{var} = r.read_varint\nr.pop_limit(limit)\npos = r.pos\n"
126
+ end
127
+ <<~RUBY
128
+ if pos < #{lim} && (#{var} = buf.getbyte(pos)) < 0x80
129
+ pos += 1
130
+ elsif pos + 1 < #{lim} && (b = buf.getbyte(pos + 1)) < 0x80
131
+ #{var} = (#{var} & 0x7F) | (b << 7)
132
+ pos += 2
133
+ else
134
+ #{fallback}end
135
+ RUBY
136
+ end
137
+
138
+ def fixed(size, directive)
139
+ <<~RUBY
140
+ raise Protostar::DecodeError.new("truncated #{size * 8}-bit value", offset: pos) if pos + #{size} > limit
141
+ v = buf.unpack1(#{directive.inspect}, offset: pos)
142
+ pos += #{size}
143
+ RUBY
144
+ end
145
+
146
+ # Source that leaves the field's next value in `v`
147
+ def read(info, lim = "limit")
148
+ case info.kind
149
+ when F::INT32 then "#{varint("v", lim)}v -= 0x1_0000_0000 if (v &= 0xFFFF_FFFF) >= 0x8000_0000\n"
150
+ when F::INT64 then "#{varint("v", lim)}v -= 0x1_0000_0000_0000_0000 if v >= 0x8000_0000_0000_0000\n"
151
+ when F::UINT32 then "#{varint("v", lim)}v &= 0xFFFF_FFFF\n"
152
+ when F::UINT64 then varint("v", lim)
153
+ when F::SINT32 then "#{varint("v", lim)}v &= 0xFFFF_FFFF\nv = (v >> 1) ^ -(v & 1)\n"
154
+ when F::SINT64 then "#{varint("v", lim)}v = (v >> 1) ^ -(v & 1)\n"
155
+ when F::BOOL then "#{varint("v", lim)}v = v != 0\n"
156
+ when F::STRING, F::BYTES then bytes(info)
157
+ else fixed(*F::FIXED.fetch(info.kind))
158
+ end
159
+ end
160
+
161
+ def bytes(info)
162
+ src = varint("n")
163
+ src << "raise Protostar::DecodeError.new(\"length \#{n} runs past the end of the input\", offset: pos) if pos + n > limit\n"
164
+ src << "v = buf.byteslice(pos, n)\npos += n\n"
165
+ if info.kind == F::BYTES
166
+ src << "v.force_encoding(Encoding::BINARY).freeze\n"
167
+ else
168
+ src << "v.force_encoding(Encoding::UTF_8)\n"
169
+ if info.utf8_validation
170
+ src << "raise Protostar::DecodeError.new(\"invalid UTF-8 in #{info.descriptor.full_name}\", offset: pos - n) " \
171
+ "unless v.valid_encoding?\n"
172
+ end
173
+ src << "v.freeze\n"
174
+ end
175
+ src
176
+ end
177
+
178
+ def enum(info)
179
+ src = info.closed_enum ? "start = pos\n#{varint("v")}" : varint("v")
180
+ src << "v -= 0x1_0000_0000 if (v &= 0xFFFF_FFFF) >= 0x8000_0000\n"
181
+ src << "if (e = self.class.__fields[#{info.index}].enum_names[v])\n"
182
+ src << store(info, "e")
183
+ src << if info.closed_enum
184
+ "else\nProtostar::Codec::Decoder.closed_enum_value(self, #{info.number}, buf, start, pos)\n"
185
+ else
186
+ "else\n#{store(info, "v")}"
187
+ end
188
+ src << "end\n"
189
+ end
190
+
191
+ def store(info, var)
192
+ i = info.index
193
+ return "(values[#{i}] ||= []) << #{var}\n" if info.repeated
194
+ siblings = info.siblings ? info.siblings.map { |s| "values[#{s}] = nil\n" }.join : ""
195
+ "#{siblings}values[#{i}] = #{var}\n"
196
+ end
197
+ end
198
+ end
199
+ end