diff --git a/CHANGELOG.md b/CHANGELOG.md index db30bb0..ad0500d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Make parsing configurable, with `parse_with_config()` and `Config`. + - Allow ignoring groups instead of always failing. + ### Changed -- Instead of failing to decode, any fields with a type of `SGROUP` or `EGROUP` - are ignored. Any nested fields are treated as if they were not nested. +- Groups now result in an `UnexpectedGroup` error instead of `UnknownWireType`. ## v1.1.0 - 2025-10-01 diff --git a/README.md b/README.md index 144eeda..04b5b3a 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,8 @@ Decode the protobuf wire format using gleam `Decoder`s! There is currently no support for encoding the protobuf wire format. -Groups (`SGROUP` and `EGROUP`) are not supported. If encountered, they are -ignored and treated as regular fields. Any fields nested within them will be -treated as if they were not nested. - +Groups (`SGROUP` and `EGROUP`) are not supported. By default, parsing fails if +one is encountered. See `Config.ignore_groups` for more information. [![Package Version](https://img.shields.io/hexpm/v/protobin)](https://hex.pm/packages/protobin) [![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/protobin/) diff --git a/src/protobin.gleam b/src/protobin.gleam index 879947b..a46d28c 100644 --- a/src/protobin.gleam +++ b/src/protobin.gleam @@ -19,6 +19,7 @@ pub type ParseError { InvalidFixed(size: Int, bits: BitArray, pos: BytePos) InvalidLen(len: Int, value_bits: BitArray, pos: BytePos) UnableToDecode(List(decode.DecodeError)) + UnexpectedGroup(wire_type: WireType, pos: BytePos) } pub type DecodeResult(t) = @@ -43,11 +44,37 @@ pub type ValueResult = pub type ValueParser = fn(BitArray, BytePos) -> ValueResult +pub type Config { + Config( + /// Whether or not to ignore groups and allow parsing to succeed if one is + /// encountered. If ignored, all nested fields will be treated as if they + /// were not nested. + /// + /// Defaults to `False`. + ignore_groups: Bool, + ) +} + +const config_default = Config(ignore_groups: False) + pub fn parse( from bits: BitArray, using decoder: Decoder(t), ) -> DecodeResult(Parsed(t)) { - use Parsed(value: data, rest:, pos:) <- result.try(read_fields(bits, [], 0)) + parse_with_config(from: bits, using: decoder, config: config_default) +} + +pub fn parse_with_config( + from bits: BitArray, + using decoder: Decoder(t), + config config: Config, +) -> DecodeResult(Parsed(t)) { + use Parsed(value: data, rest:, pos:) <- result.try(read_fields( + bits, + [], + 0, + config, + )) decode.run(data, decoder) |> result.map(fn(value) { Parsed(value:, rest:, pos:) }) |> result.map_error(UnableToDecode) @@ -57,6 +84,7 @@ fn read_fields( bits: BitArray, acc: List(Field), pos: BytePos, + config: Config, ) -> DecodeResult(Parsed(Dynamic)) { case bits { <<>> -> @@ -70,8 +98,9 @@ fn read_fields( use Parsed(value: prop, rest: bits, pos:) <- result.try(read_field( bits, pos, + config, )) - read_fields(bits, [prop, ..acc], pos) + read_fields(bits, [prop, ..acc], pos, config) } } } @@ -99,7 +128,11 @@ fn repeated_to_list(reversed_fields: List(Field)) -> dict.Dict(Dynamic, Dynamic) dict.map_values(in: fields, with: fn(_key, field) { field |> dynamic.list }) } -fn read_field(bits: BitArray, tag_pos: BytePos) -> DecodeResult(Parsed(Field)) { +fn read_field( + bits: BitArray, + tag_pos: BytePos, + config: Config, +) -> DecodeResult(Parsed(Field)) { use Parsed(value: tag, rest: bits, pos:) <- result.try(parse_varint( bits, tag_pos, @@ -114,6 +147,16 @@ fn read_field(bits: BitArray, tag_pos: BytePos) -> DecodeResult(Parsed(Field)) { UnknownWireType(wire_type, pos: tag_pos), )) + // Fail when groups are found + use wire_type <- result.try({ + case wire_type, config { + wire_type.SGroup, Config(ignore_groups: False) + | wire_type.EGroup, Config(ignore_groups: False) + -> Error(UnexpectedGroup(wire_type:, pos: tag_pos)) + _, _ -> Ok(wire_type) + } + }) + let read = wire_type_read_fn(wire_type) use value: Parsed(Dynamic) <- result.try( read(bits, pos) |> result.map(parsed_map(_, dynamic.bit_array)), diff --git a/test/protobin_test.gleam b/test/protobin_test.gleam index ff26fd6..ea083be 100644 --- a/test/protobin_test.gleam +++ b/test/protobin_test.gleam @@ -1,9 +1,10 @@ import gleam/dynamic/decode.{type Decoder} import gleam/option import gleeunit +import protobin/internal/wire_type import simplifile as file -import protobin.{Parsed, parse, read_fixed, read_varint} +import protobin.{Parsed, parse, parse_with_config, read_fixed, read_varint} pub fn main() -> Nil { gleeunit.main() @@ -267,7 +268,11 @@ pub fn ignore_groups_test() { let path = "./test/ignores-groups.pb" let assert Ok(bits) = file.read_bits(from: path) let assert Ok(ignores_groups) = - parse(from: bits, using: ignores_groups_decoder()) + parse_with_config( + from: bits, + using: ignores_groups_decoder(), + config: protobin.Config(ignore_groups: True), + ) assert ignores_groups == Parsed( @@ -276,3 +281,11 @@ pub fn ignore_groups_test() { pos: 16, ) } + +pub fn does_not_ignore_groups_test() { + let path = "./test/ignores-groups.pb" + let assert Ok(bits) = file.read_bits(from: path) + let assert Error(err) = parse(from: bits, using: ignores_groups_decoder()) + + assert err == protobin.UnexpectedGroup(wire_type: wire_type.SGroup, pos: 2) +}