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,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Protostar
4
+ module WellKnown
5
+ # Mixed into google.protobuf.Timestamp
6
+ module Timestamp
7
+ def self.included(base)
8
+ base.extend(ClassMethods)
9
+ end
10
+
11
+ # @param value [Object]
12
+ # @return [Array(Integer, Integer), nil] seconds and nanos of a Time, a
13
+ # DateTime or a Date; nil for anything else
14
+ def self.parts(value)
15
+ time = if value.is_a?(::Time) then value
16
+ elsif defined?(::Date) && value.is_a?(::Date) then value.to_time
17
+ else return nil
18
+ end
19
+ [time.to_i, time.nsec]
20
+ end
21
+
22
+ module ClassMethods
23
+ # @param time [Time]
24
+ # @return [Message] a new Timestamp at the same instant, to the nanosecond
25
+ def from_time(time)
26
+ new.from_time(time)
27
+ end
28
+
29
+ # What a Timestamp field makes of a value that is neither a Timestamp
30
+ # nor a Hash: a Time, a DateTime or a Date becomes a Timestamp
31
+ # @return [Message, nil]
32
+ # @api private
33
+ def __coerce(value)
34
+ seconds, nanos = Timestamp.parts(value)
35
+ new(seconds: seconds, nanos: nanos) if seconds
36
+ end
37
+ end
38
+
39
+ # @param time [Time]
40
+ # @return [self]
41
+ def from_time(time)
42
+ self.seconds = time.to_i
43
+ self.nanos = time.nsec
44
+ self
45
+ end
46
+
47
+ # @return [Time] in the local zone, as `Time.at` gives it
48
+ def to_time
49
+ ::Time.at(seconds, nanos, :nanosecond)
50
+ end
51
+
52
+ # @return [Integer] seconds since the epoch
53
+ def to_i
54
+ seconds
55
+ end
56
+
57
+ # @return [Rational] seconds since the epoch, exact; google-protobuf
58
+ # returns a Rational here too
59
+ def to_f
60
+ seconds + nanos.quo(1_000_000_000)
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Protostar
4
+ module WellKnown
5
+ # Mixed into google.protobuf.Value
6
+ module Value
7
+ def self.included(base)
8
+ base.extend(ClassMethods)
9
+ end
10
+
11
+ module ClassMethods
12
+ # @param value [Object] nil, Numeric, String, true, false, Hash,
13
+ # Array, a Struct or a ListValue
14
+ # @return [Message] a new Value
15
+ # @raise [UnexpectedStructType]
16
+ def from_ruby(value)
17
+ new.from_ruby(value)
18
+ end
19
+ end
20
+
21
+ # @param recursive [Boolean] whether to convert a Struct or ListValue to
22
+ # a Hash or Array as well
23
+ # @return [Object]
24
+ # @raise [UnexpectedStructType] when no kind is set
25
+ def to_ruby(recursive = false)
26
+ case which_kind
27
+ when :struct_value then recursive ? struct_value.to_h : struct_value
28
+ when :list_value then recursive ? list_value.to_a : list_value
29
+ when :null_value then nil
30
+ when :number_value then number_value
31
+ when :string_value then string_value
32
+ when :bool_value then bool_value
33
+ else raise UnexpectedStructType
34
+ end
35
+ end
36
+
37
+ # Sets the kind matching a Ruby value
38
+ # @return [self]
39
+ # @raise [UnexpectedStructType]
40
+ def from_ruby(value)
41
+ fields = self.class.__by_name
42
+ struct_class = fields.fetch("struct_value").message_class
43
+ list_class = fields.fetch("list_value").message_class
44
+ case value
45
+ when NilClass then self.null_value = :NULL_VALUE
46
+ when Numeric then self.number_value = value.to_f
47
+ when String then self.string_value = value
48
+ when TrueClass then self.bool_value = true
49
+ when FalseClass then self.bool_value = false
50
+ when struct_class then self.struct_value = value
51
+ when Hash then self.struct_value = struct_class.from_hash(value)
52
+ when list_class then self.list_value = value
53
+ when Array then self.list_value = list_class.from_a(value)
54
+ else raise UnexpectedStructType
55
+ end
56
+ self
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Protostar
4
+ # Ruby helpers for the well-known types, with the names google-protobuf's
5
+ # `well_known_types.rb` gives them. {Types.define} includes the matching
6
+ # mixin into a class whose full name is in {MIXINS}, so the helpers are
7
+ # there however the class came to be defined.
8
+ #
9
+ # any = Google::Protobuf::Any.pack(person)
10
+ # any.unpack(Person) # => person
11
+ # Google::Protobuf::Timestamp.from_time(Time.now).to_time
12
+ # Google::Protobuf::Struct.from_hash("a" => [1, nil]).to_h
13
+ module WellKnown
14
+ # Raised by Struct, Value and ListValue for a Ruby value they cannot hold
15
+ class UnexpectedStructType < Protostar::Error; end
16
+ end
17
+ end
18
+
19
+ require_relative "well_known/any"
20
+ require_relative "well_known/timestamp"
21
+ require_relative "well_known/duration"
22
+ require_relative "well_known/struct"
23
+ require_relative "well_known/value"
24
+ require_relative "well_known/list_value"
25
+
26
+ module Protostar
27
+ module WellKnown
28
+ # Full name to the mixin {Types.define} includes into that class
29
+ MIXINS = {
30
+ "google.protobuf.Any" => Any,
31
+ "google.protobuf.Timestamp" => Timestamp,
32
+ "google.protobuf.Duration" => Duration,
33
+ "google.protobuf.Struct" => Struct,
34
+ "google.protobuf.Value" => Value,
35
+ "google.protobuf.ListValue" => ListValue
36
+ }.freeze
37
+ end
38
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Protostar
4
+ module Wire
5
+ # Presents an IO::Buffer with the String methods {Reader} uses, so that
6
+ # a buffer is read in place: numbers with `get_value`, and only string
7
+ # and bytes values copied out, with `get_string`.
8
+ class IOSource
9
+ # unpack1 directives the codec uses, as IO::Buffer data types
10
+ TYPES = {"V" => :u32, "Q<" => :u64, "l<" => :s32, "q<" => :s64, "e" => :f32, "E" => :f64}.freeze
11
+
12
+ # @return [IO::Buffer]
13
+ attr_reader :buffer
14
+ # @return [Integer]
15
+ attr_reader :bytesize
16
+
17
+ # @param buffer [IO::Buffer] must not change while it is read
18
+ def initialize(buffer)
19
+ @buffer = buffer
20
+ @bytesize = buffer.size
21
+ freeze
22
+ end
23
+
24
+ # @return [Integer]
25
+ def getbyte(offset)
26
+ @buffer.get_value(:U8, offset)
27
+ end
28
+
29
+ # @return [String] a new binary String
30
+ def byteslice(offset, length)
31
+ @buffer.get_string(offset, length)
32
+ end
33
+
34
+ # @param format [String] one of the directives in {TYPES}
35
+ def unpack1(format, offset:)
36
+ @buffer.get_value(TYPES.fetch(format), offset)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,254 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Protostar
4
+ module Wire
5
+ # Reads wire-format values from a String without copying it or allocating
6
+ # per byte. The String is never mutated, and its encoding does not matter:
7
+ # every read works on bytes. An IO::Buffer is read in place through an
8
+ # {IOSource}, and so is any source answering `getbyte`, `byteslice`,
9
+ # `unpack1(format, offset:)` and `bytesize` the way String does.
10
+ #
11
+ # A reader has a position and a limit. Reads never go past the limit, so a
12
+ # nested message is read by narrowing the limit to its end and restoring it
13
+ # afterwards ({#push_limit}, {#pop_limit}).
14
+ #
15
+ # r = Protostar::Wire::Reader.new(bytes)
16
+ # until r.eof?
17
+ # number, wire_type = r.read_tag
18
+ # ...
19
+ # end
20
+ class Reader
21
+ # What a source other than a String must answer
22
+ SOURCE_METHODS = %i[getbyte byteslice unpack1 bytesize].freeze
23
+
24
+ # @return [String, IOSource, Object] the underlying bytes
25
+ attr_reader :buffer
26
+ # @return [Integer] the current byte offset
27
+ attr_accessor :pos
28
+ # @return [Integer] the offset reads must not pass
29
+ attr_reader :limit
30
+
31
+ # @param buffer [String, IO::Buffer, #getbyte] an IO::Buffer is wrapped in an {IOSource}, not copied
32
+ # @param pos [Integer] where to start
33
+ # @param limit [Integer, nil] where to stop, the end of the buffer by default
34
+ def initialize(buffer, pos = 0, limit = nil)
35
+ buffer = IOSource.new(buffer) if defined?(IO::Buffer) && buffer.is_a?(IO::Buffer)
36
+ unless buffer.is_a?(String) || SOURCE_METHODS.all? { |m| buffer.respond_to?(m) }
37
+ raise ::TypeError, "expected a String, an IO::Buffer or a source answering #{SOURCE_METHODS.join(", ")}, got #{buffer.class}"
38
+ end
39
+ @buffer = buffer
40
+ @pos = pos
41
+ @limit = limit || buffer.bytesize
42
+ raise ArgumentError, "limit #{@limit} is past the end of the buffer" if @limit > buffer.bytesize
43
+ end
44
+
45
+ # @return [Boolean] whether the position has reached the limit
46
+ def eof?
47
+ @pos >= @limit
48
+ end
49
+
50
+ # Narrows the limit to `length` bytes from the current position
51
+ # @return [Integer] the previous limit, to hand to {#pop_limit}
52
+ # @raise [DecodeError] when that runs past the current limit
53
+ def push_limit(length)
54
+ new_limit = @pos + length
55
+ raise DecodeError.new("length #{length} runs past the end of the input", offset: @pos) if new_limit > @limit
56
+ old = @limit
57
+ @limit = new_limit
58
+ old
59
+ end
60
+
61
+ # Restores a limit returned by {#push_limit}
62
+ def pop_limit(old_limit)
63
+ @limit = old_limit
64
+ end
65
+
66
+ # @return [Integer] an unsigned varint of up to 64 bits. Longer encodings
67
+ # are truncated to 64 bits, as protoc does.
68
+ # @raise [DecodeError] when truncated or longer than ten bytes
69
+ def read_varint
70
+ pos = @pos
71
+ raise DecodeError.new("truncated varint", offset: pos) if pos >= @limit
72
+ byte = @buffer.getbyte(pos)
73
+ if byte < 0x80
74
+ @pos = pos + 1
75
+ return byte
76
+ end
77
+ read_varint_slow(pos)
78
+ end
79
+
80
+ # @return [Integer] a tag's varint, unchecked
81
+ # @raise [DecodeError] when truncated or longer than five bytes, which
82
+ # protoc refuses even when the value would fit
83
+ def read_tag_varint
84
+ at = @pos
85
+ tag = read_varint
86
+ raise DecodeError.new("tag longer than 5 bytes", offset: at) if @pos - at > 5
87
+ tag
88
+ end
89
+
90
+ # @return [Array(Integer, Integer)] field number and wire type
91
+ # @raise [DecodeError] for field number 0, an out of range tag or an invalid wire type
92
+ def read_tag
93
+ at = @pos
94
+ tag = read_tag_varint
95
+ Reader.check_tag(tag, at)
96
+ [tag >> 3, tag & 7]
97
+ end
98
+
99
+ # @return [Integer] a signed 32-bit integer from a varint (int32 and enum)
100
+ def read_int32
101
+ v = read_varint & MASK32
102
+ (v >= 0x8000_0000) ? v - 0x1_0000_0000 : v
103
+ end
104
+
105
+ # @return [Integer] a signed 64-bit integer from a varint
106
+ def read_int64
107
+ v = read_varint
108
+ (v >= 0x8000_0000_0000_0000) ? v - 0x1_0000_0000_0000_0000 : v
109
+ end
110
+
111
+ # @return [Integer] an unsigned 32-bit integer from a varint
112
+ def read_uint32
113
+ read_varint & MASK32
114
+ end
115
+
116
+ # @return [Integer] a zigzag-decoded signed 32-bit integer (sint32)
117
+ def read_sint32
118
+ v = read_varint & MASK32
119
+ (v >> 1) ^ -(v & 1)
120
+ end
121
+
122
+ # @return [Integer] a zigzag-decoded signed 64-bit integer (sint64)
123
+ def read_sint64
124
+ v = read_varint
125
+ (v >> 1) ^ -(v & 1)
126
+ end
127
+
128
+ # @return [Integer] an unsigned 32-bit little-endian integer
129
+ def read_fixed32
130
+ read_fixed(4, "V")
131
+ end
132
+
133
+ # @return [Integer] an unsigned 64-bit little-endian integer
134
+ def read_fixed64
135
+ read_fixed(8, "Q<")
136
+ end
137
+
138
+ # @return [Integer]
139
+ def read_sfixed32
140
+ read_fixed(4, "l<")
141
+ end
142
+
143
+ # @return [Integer]
144
+ def read_sfixed64
145
+ read_fixed(8, "q<")
146
+ end
147
+
148
+ # @return [Float]
149
+ def read_float
150
+ read_fixed(4, "e")
151
+ end
152
+
153
+ # @return [Float]
154
+ def read_double
155
+ read_fixed(8, "E")
156
+ end
157
+
158
+ # Reads a varint length and that many bytes
159
+ # @return [String] a new binary String
160
+ def read_bytes
161
+ len = read_varint
162
+ read_raw(len)
163
+ end
164
+
165
+ # @return [String] the next `length` bytes as a new binary String
166
+ def read_raw(length)
167
+ pos = @pos
168
+ raise DecodeError.new("length #{length} runs past the end of the input", offset: pos) if pos + length > @limit
169
+ @pos = pos + length
170
+ s = @buffer.byteslice(pos, length)
171
+ s.force_encoding(BINARY)
172
+ end
173
+
174
+ # Skips the value of a field whose tag was just read
175
+ # @param number [Integer] the field number, needed to match the end of a group
176
+ # @param wire_type [Integer]
177
+ # @param depth [Integer] groups left before {DecodeError} for nesting too deep
178
+ def skip_field(number, wire_type, depth = 100)
179
+ case wire_type
180
+ when VARINT then read_varint
181
+ when FIXED64 then skip(8)
182
+ when LENGTH_DELIMITED then skip(read_varint)
183
+ when FIXED32 then skip(4)
184
+ when START_GROUP then skip_group(number, depth)
185
+ when END_GROUP then raise DecodeError.new("unmatched end of group #{number}", offset: @pos)
186
+ else raise DecodeError.new("invalid wire type #{wire_type}", offset: @pos)
187
+ end
188
+ nil
189
+ end
190
+
191
+ # @raise [DecodeError] unless `length` bytes remain before the limit
192
+ def skip(length)
193
+ raise DecodeError.new("length #{length} runs past the end of the input", offset: @pos) if @pos + length > @limit
194
+ @pos += length
195
+ end
196
+
197
+ # Skips the contents of a group up to and including its end tag
198
+ def skip_group(number, depth = 100)
199
+ raise DecodeError.new("groups nested too deep", offset: @pos) if depth <= 0
200
+ loop do
201
+ raise DecodeError.new("missing end of group #{number}", offset: @pos) if @pos >= @limit
202
+ at = @pos
203
+ tag = read_tag_varint
204
+ Reader.check_tag(tag, at)
205
+ n = tag >> 3
206
+ wt = tag & 7
207
+ if wt == END_GROUP
208
+ return if n == number
209
+ raise DecodeError.new("end of group #{n} does not match group #{number}", offset: at)
210
+ end
211
+ skip_field(n, wt, depth - 1)
212
+ end
213
+ end
214
+
215
+ # @api private
216
+ def self.check_tag(tag, at)
217
+ raise DecodeError.new("tag out of range", offset: at) if tag > MASK32
218
+ raise DecodeError.new("field number 0", offset: at) if tag < 8
219
+ wt = tag & 7
220
+ raise DecodeError.new("invalid wire type #{wt}", offset: at) if wt > 5
221
+ end
222
+
223
+ private
224
+
225
+ def read_fixed(size, format)
226
+ pos = @pos
227
+ raise DecodeError.new("truncated #{size * 8}-bit value", offset: pos) if pos + size > @limit
228
+ @pos = pos + size
229
+ @buffer.unpack1(format, offset: pos)
230
+ end
231
+
232
+ def read_varint_slow(start)
233
+ buf = @buffer
234
+ limit = @limit
235
+ pos = start
236
+ result = 0
237
+ shift = 0
238
+ while shift < 70
239
+ raise DecodeError.new("truncated varint", offset: start) if pos >= limit
240
+ byte = buf.getbyte(pos)
241
+ pos += 1
242
+ result |= (byte & 0x7F) << shift
243
+ if byte < 0x80
244
+ @pos = pos
245
+ # masking with a Bignum allocates, and only a tenth byte can overflow 64 bits
246
+ return (shift == 63) ? result & MASK64 : result
247
+ end
248
+ shift += 7
249
+ end
250
+ raise DecodeError.new("varint longer than 10 bytes", offset: start)
251
+ end
252
+ end
253
+ end
254
+ end
@@ -0,0 +1,201 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Protostar
4
+ module Wire
5
+ # Writes wire-format values into a binary String, or through a small
6
+ # buffer into a sink: an IO, or anything that takes bytes.
7
+ #
8
+ # w = Protostar::Wire::Writer.new
9
+ # w.write_tag(1, Protostar::Wire::VARINT).write_varint(150)
10
+ # w.string # => "\x08\x96\x01"
11
+ #
12
+ # Protostar::Wire::Writer.new(io).write_bytes("hi").flush
13
+ class Writer
14
+ # Bytes buffered before a sink is written to, by default
15
+ FLUSH_AT = 64 * 1024
16
+
17
+ # @return [String] the bytes written so far (for a sink, those not yet flushed)
18
+ attr_reader :string
19
+ # @return [Object, nil] the sink, or nil for a String target
20
+ attr_reader :io
21
+ # @return [Integer] bytes buffered before the sink is written to
22
+ attr_reader :flush_at
23
+
24
+ # @param target [String, #write, #<<, #call, nil] a String to append to
25
+ # (it is switched to binary encoding), a sink, or nil for a new String.
26
+ # A sink is anything answering `write` (an IO, a socket, a Rack or
27
+ # ActionController::Live stream), `<<` (an Array, an Enumerator::Yielder)
28
+ # or `call` (a Proc). An IO or StringIO is given the buffer itself and
29
+ # anything else a copy, since a sink may keep what it is given.
30
+ # @param flush_at [Integer] bytes to collect before writing to the sink;
31
+ # 0 writes on every {#flush_if_full}
32
+ def initialize(target = nil, flush_at: FLUSH_AT)
33
+ @flush_at = flush_at
34
+ if target.nil?
35
+ @string = String.new(encoding: BINARY)
36
+ @io = nil
37
+ elsif target.is_a?(String)
38
+ @string = target.force_encoding(BINARY)
39
+ @io = nil
40
+ else
41
+ @string = String.new(capacity: (flush_at > 0) ? flush_at : 256, encoding: BINARY)
42
+ @io = target
43
+ @push = Writer.push_method(target)
44
+ @copy = !(target.is_a?(IO) || (defined?(::StringIO) && target.is_a?(::StringIO)))
45
+ end
46
+ end
47
+
48
+ # @return [Symbol] how bytes are handed to `sink`: :write, :<< or :call
49
+ # @raise [ArgumentError] when it takes none of them
50
+ def self.push_method(sink)
51
+ # Proc#<< composes, so a callable is called before anything else is tried
52
+ if sink.is_a?(Proc) || sink.is_a?(Method) then :call
53
+ elsif sink.respond_to?(:write) then :write
54
+ elsif sink.respond_to?(:<<) then :<<
55
+ elsif sink.respond_to?(:call) then :call
56
+ else
57
+ raise ArgumentError, "#{sink.class} is not a sink: it answers none of write, << and call"
58
+ end
59
+ end
60
+
61
+ # @param value [Integer] negative values are written as ten-byte two's complement
62
+ # @return [self]
63
+ def write_varint(value)
64
+ buf = @string
65
+ if value < 0x80 && value >= 0
66
+ buf << value
67
+ return self
68
+ end
69
+ value &= MASK64 if value < 0
70
+ while value >= 0x80
71
+ buf << ((value & 0x7F) | 0x80)
72
+ value >>= 7
73
+ end
74
+ buf << value
75
+ self
76
+ end
77
+
78
+ # @return [self]
79
+ def write_tag(number, wire_type)
80
+ write_varint((number << 3) | wire_type)
81
+ end
82
+
83
+ # @param value [Integer] a signed integer, written zigzag-encoded (sint32, sint64)
84
+ # @return [self]
85
+ def write_zigzag(value)
86
+ write_varint((value << 1) ^ (value >> 63))
87
+ end
88
+
89
+ # @return [self]
90
+ def write_fixed32(value)
91
+ [value].pack("V", buffer: @string)
92
+ self
93
+ end
94
+
95
+ # @return [self]
96
+ def write_fixed64(value)
97
+ [value].pack("Q<", buffer: @string)
98
+ self
99
+ end
100
+
101
+ # @return [self]
102
+ def write_sfixed32(value)
103
+ [value].pack("l<", buffer: @string)
104
+ self
105
+ end
106
+
107
+ # @return [self]
108
+ def write_sfixed64(value)
109
+ [value].pack("q<", buffer: @string)
110
+ self
111
+ end
112
+
113
+ # @return [self]
114
+ def write_float(value)
115
+ if value.is_a?(Float) && value.nan?
116
+ @string << Wire.float_nan(value)
117
+ else
118
+ [value].pack("e", buffer: @string)
119
+ end
120
+ self
121
+ end
122
+
123
+ # @return [self]
124
+ def write_double(value)
125
+ [value].pack("E", buffer: @string)
126
+ self
127
+ end
128
+
129
+ # Writes a varint length followed by the bytes of `value`, whatever its encoding
130
+ # @return [self]
131
+ def write_bytes(value)
132
+ write_varint(value.bytesize)
133
+ write_raw(value)
134
+ end
135
+
136
+ if String.method_defined?(:append_as_bytes)
137
+ # Appends the bytes of `value` as they are, whatever its encoding
138
+ # @return [self]
139
+ def write_raw(value)
140
+ @string.append_as_bytes(value)
141
+ flush_if_full
142
+ end
143
+ else
144
+ # Appends the bytes of `value` as they are, whatever its encoding
145
+ # @return [self]
146
+ def write_raw(value)
147
+ # `<<` would raise or re-tag the buffer for a non-ASCII UTF-8 value
148
+ [value].pack("a*", buffer: @string)
149
+ flush_if_full
150
+ end
151
+ end
152
+
153
+ # Writes the buffer to the sink once it holds {#flush_at} bytes
154
+ # @return [self]
155
+ def flush_if_full
156
+ flush if @io && @string.bytesize >= @flush_at
157
+ self
158
+ end
159
+
160
+ # Writes whatever is buffered to the sink. A no-op for a String target.
161
+ # @return [self]
162
+ def flush
163
+ if @io && !@string.empty?
164
+ deliver(@copy ? @string.dup : @string)
165
+ @string.clear
166
+ end
167
+ self
168
+ end
169
+
170
+ # Writes the buffer and then `bytes` to the sink, without copying
171
+ # `bytes` into the buffer first. For a String target, appends them.
172
+ # @return [self]
173
+ def write_through(bytes)
174
+ return write_raw(bytes) unless @io
175
+ if @copy
176
+ flush
177
+ deliver(bytes.b)
178
+ else
179
+ # IO#write and StringIO#write take several Strings in one call
180
+ deliver_all(@string, bytes)
181
+ @string.clear
182
+ end
183
+ self
184
+ end
185
+
186
+ private
187
+
188
+ def deliver_all(*chunks)
189
+ @io.write(*chunks)
190
+ end
191
+
192
+ def deliver(bytes)
193
+ case @push
194
+ when :write then @io.write(bytes)
195
+ when :<< then @io << bytes
196
+ else @io.call(bytes)
197
+ end
198
+ end
199
+ end
200
+ end
201
+ end