Packages

Pure Gleam implementation of the Apache Thrift Compact Protocol

Current section

Files

Jump to
thrifty src thrifty@reader.erl
Raw

src/thrifty@reader.erl

-module(thrifty@reader).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/thrifty/reader.gleam").
-export([from_bit_array/1, with_options/2, position/1, read_i8/1, read_varint/1, read_i16/1, read_i32/1, read_i64/1, read_double/1, read_binary/1, read_string/1, read_bool_element/1, skip_value/2, read_struct/1]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
-file("src/thrifty/reader.gleam", 41).
?DOC(
" Helpers for reading Compact Protocol values with an immutable `Reader`.\n"
"\n"
" Public API contract (high level):\n"
" - from_bit_array/1: create a `Reader` positioned at the beginning of `data`.\n"
" - read_i8/read_i16/read_i32/read_i64: read the corresponding signed\n"
" integer and return Ok(#(value, reader)) or an Error(DecodeError).\n"
" - read_double: read IEEE-754 little-endian double returning Ok(#(float, reader)).\n"
" - read_binary/read_string: read a length-prefixed binary (varint length)\n"
" and return Ok(#(bytes, reader)) or Ok(#(string, reader)) for strings.\n"
" - read_struct: parse a struct returning the list of field headers and the\n"
" reader positioned after the struct payload.\n"
" - skip_value: skip a value of the given field type efficiently and return\n"
" the advanced reader.\n"
"\n"
" All public readers return `Result(#(T, types.Reader), types.DecodeError)` to\n"
" make incremental parsing and error handling explicit. Examples and common\n"
" error cases are documented on specific functions below.\n"
).
-spec from_bit_array(bitstring()) -> thrifty@types:reader().
from_bit_array(Data) ->
{reader,
Data,
0,
{reader_options, 64, 65536, 8388608, accept_canonical_only}}.
-file("src/thrifty/reader.gleam", 45).
-spec with_options(bitstring(), thrifty@types:reader_options()) -> thrifty@types:reader().
with_options(Data, Options) ->
{reader, Data, 0, Options}.
-file("src/thrifty/reader.gleam", 53).
?DOC(" Current byte offset inside the reader.\n").
-spec position(thrifty@types:reader()) -> integer().
position(Reader) ->
{reader, _, Byte_pos, _} = Reader,
Byte_pos.
-file("src/thrifty/reader.gleam", 534).
?DOC(
" Set the reader's byte position and options, returning a new immutable reader.\n"
"\n"
" Inputs\n"
" - `reader`: existing `types.Reader` instance.\n"
" - `byte_pos`: new byte offset to set.\n"
" - `options`: `types.ReaderOptions` to attach to the new reader.\n"
"\n"
" Outputs\n"
" - A `types.Reader` referencing the same underlying data with updated\n"
" position and options.\n"
).
-spec set_position(
thrifty@types:reader(),
integer(),
thrifty@types:reader_options()
) -> thrifty@types:reader().
set_position(Reader, Byte_pos, Options) ->
{reader, Data, _, _} = Reader,
{reader, Data, Byte_pos, Options}.
-file("src/thrifty/reader.gleam", 59).
?DOC(" Read an i8 value.\n").
-spec read_i8(thrifty@types:reader()) -> {ok,
{integer(), thrifty@types:reader()}} |
{error, thrifty@types:decode_error()}.
read_i8(Reader) ->
{reader, Data, Byte_pos, Options} = Reader,
case gleam_stdlib:bit_array_slice(Data, Byte_pos, 1) of
{error, _} ->
{error, unexpected_end_of_input};
{ok, Bits} ->
case Bits of
<<Value:8/integer-signed>> ->
{ok, {Value, set_position(Reader, Byte_pos + 1, Options)}};
_ ->
{error, {invalid_wire_format, <<"Invalid byte"/utf8>>}}
end
end.
-file("src/thrifty/reader.gleam", 109).
-spec read_varint(thrifty@types:reader()) -> {ok,
{integer(), thrifty@types:reader()}} |
{error, thrifty@types:decode_error()}.
read_varint(Reader) ->
{reader, Data, Byte_pos, Options} = Reader,
case thrifty@varint:decode_varint(Data, Byte_pos) of
{error, E} ->
{error, E};
{ok, {Value, Next_pos}} ->
{ok, {Value, set_position(Reader, Next_pos, Options)}}
end.
-file("src/thrifty/reader.gleam", 502).
?DOC(
" Skip `count` bytes in the reader, returning an advanced reader.\n"
"\n"
" Inputs\n"
" - `reader`: current `types.Reader`.\n"
" - `count`: number of bytes to skip.\n"
"\n"
" Outputs\n"
" - `Ok(types.Reader)` advanced `count` bytes on success.\n"
" - `Error(types.UnexpectedEndOfInput)` when there are not enough bytes.\n"
).
-spec read_zigzag_integer(thrifty@types:reader(), fun((integer()) -> integer())) -> {ok,
{integer(), thrifty@types:reader()}} |
{error, thrifty@types:decode_error()}.
read_zigzag_integer(Reader, Decode) ->
case read_varint(Reader) of
{error, E} ->
{error, E};
{ok, {Bits, Next_reader}} ->
{ok, {Decode(Bits), Next_reader}}
end.
-file("src/thrifty/reader.gleam", 76).
?DOC(
" Read a zigzag-encoded i16/i32/i64 depending on the helper used. Returns\n"
" Ok(#(value, reader)) on success or Error(types.DecodeError) on failure.\n"
).
-spec read_i16(thrifty@types:reader()) -> {ok,
{integer(), thrifty@types:reader()}} |
{error, thrifty@types:decode_error()}.
read_i16(Reader) ->
read_zigzag_integer(Reader, fun thrifty@zigzag:decode_i32/1).
-file("src/thrifty/reader.gleam", 82).
-spec read_i32(thrifty@types:reader()) -> {ok,
{integer(), thrifty@types:reader()}} |
{error, thrifty@types:decode_error()}.
read_i32(Reader) ->
read_zigzag_integer(Reader, fun thrifty@zigzag:decode_i32/1).
-file("src/thrifty/reader.gleam", 88).
-spec read_i64(thrifty@types:reader()) -> {ok,
{integer(), thrifty@types:reader()}} |
{error, thrifty@types:decode_error()}.
read_i64(Reader) ->
read_zigzag_integer(Reader, fun thrifty@zigzag:decode_i64/1).
-file("src/thrifty/reader.gleam", 94).
-spec read_double(thrifty@types:reader()) -> {ok,
{float(), thrifty@types:reader()}} |
{error, thrifty@types:decode_error()}.
read_double(Reader) ->
{reader, Data, Byte_pos, Options} = Reader,
case gleam_stdlib:bit_array_slice(Data, Byte_pos, 8) of
{error, _} ->
{error, unexpected_end_of_input};
{ok, Bits} ->
case Bits of
<<Value:64/float-little>> ->
{ok, {Value, set_position(Reader, Byte_pos + 8, Options)}};
_ ->
{error, {invalid_wire_format, <<"Invalid double"/utf8>>}}
end
end.
-file("src/thrifty/reader.gleam", 574).
?DOC(
" Ensure a numeric `value` does not exceed a configured `limit`.\n"
"\n"
" Inputs\n"
" - `value`: measured quantity (for example container size).\n"
" - `limit`: configured maximum allowed value.\n"
" - `message`: error message to use when the limit is exceeded.\n"
"\n"
" Outputs\n"
" - `Ok(Nil)` when `value <= limit`.\n"
" - `Error(types.InvalidWireFormat(message))` when `value > limit`.\n"
).
-spec ensure_limit(integer(), integer(), binary()) -> {ok, nil} |
{error, thrifty@types:decode_error()}.
ensure_limit(Value, Limit, Message) ->
case Value > Limit of
true ->
{error, {invalid_wire_format, Message}};
false ->
{ok, nil}
end.
-file("src/thrifty/reader.gleam", 120).
-spec read_binary(thrifty@types:reader()) -> {ok,
{bitstring(), thrifty@types:reader()}} |
{error, thrifty@types:decode_error()}.
read_binary(Reader) ->
{reader, Data, Byte_pos, Options} = Reader,
case thrifty@varint:decode_varint(Data, Byte_pos) of
{error, E} ->
{error, E};
{ok, {Length, Next_pos}} ->
case ensure_limit(
Length,
erlang:element(4, Options),
<<"Binary length exceeds max_string_bytes"/utf8>>
) of
{error, E@1} ->
{error, E@1};
{ok, nil} ->
case gleam_stdlib:bit_array_slice(Data, Next_pos, Length) of
{error, _} ->
{error, unexpected_end_of_input};
{ok, Bytes} ->
{ok,
{Bytes,
set_position(
Reader,
Next_pos + Length,
Options
)}}
end
end
end.
-file("src/thrifty/reader.gleam", 153).
?DOC(
" Read a UTF-8 string. This uses `read_binary/1` and then converts to a\n"
" Gleam `String`. Returns Error(types.InvalidWireFormat) if bytes are not valid\n"
" UTF-8. Example:\n"
"\n"
" let r0 = reader.from_bit_array(writer.write_string(\"hello\"))\n"
" let Ok(#(s, r1)) = reader.read_string(r0)\n"
" s |> should.equal(\"hello\")\n"
).
-spec read_string(thrifty@types:reader()) -> {ok,
{binary(), thrifty@types:reader()}} |
{error, thrifty@types:decode_error()}.
read_string(Reader) ->
case read_binary(Reader) of
{error, E} ->
{error, E};
{ok, {Bytes, Next_reader}} ->
case gleam@bit_array:to_string(Bytes) of
{error, _} ->
{error,
{invalid_wire_format, <<"Invalid UTF-8 string"/utf8>>}};
{ok, Value} ->
{ok, {Value, Next_reader}}
end
end.
-file("src/thrifty/reader.gleam", 181).
?DOC(
" Read a boolean element stored as a single byte (used for boolean values\n"
" inside containers such as list<bool> / set<bool> / map values).\n"
"\n"
" Encoding & canonical mapping:\n"
" - Valid element bytes are `1` and `2`. This library interprets them as\n"
" `1 -> True` and `2 -> False` (matching the inline boolean type nibble\n"
" mapping used in field headers).\n"
"\n"
" Validation behaviour:\n"
" - `types.ReaderOptions.bool_element_policy` controls validation. See\n"
" `thrifty/types.gleam` for the available `BoolElementPolicy` values.\n"
"\n"
" This function returns Ok(#(Bool, Reader)) on success, advancing the\n"
" reader past the element byte, or Error(types.DecodeError) on invalid or\n"
" truncated input.\n"
).
-spec read_bool_element(thrifty@types:reader()) -> {ok,
{boolean(), thrifty@types:reader()}} |
{error, thrifty@types:decode_error()}.
read_bool_element(Reader) ->
{reader, Data, Byte_pos, Options} = Reader,
case gleam_stdlib:bit_array_slice(Data, Byte_pos, 1) of
{error, _} ->
{error, unexpected_end_of_input};
{ok, Bits} ->
case Bits of
<<Value:8/integer>> ->
case erlang:element(5, Options) of
accept_canonical_only ->
case Value of
1 ->
{ok,
{true,
set_position(
Reader,
Byte_pos + 1,
Options
)}};
2 ->
{ok,
{false,
set_position(
Reader,
Byte_pos + 1,
Options
)}};
_ ->
{error,
{invalid_wire_format,
<<"Invalid boolean element"/utf8>>}}
end;
accept_both ->
case Value of
0 ->
{ok,
{false,
set_position(
Reader,
Byte_pos + 1,
Options
)}};
1 ->
{ok,
{true,
set_position(
Reader,
Byte_pos + 1,
Options
)}};
2 ->
{ok,
{false,
set_position(
Reader,
Byte_pos + 1,
Options
)}};
_ ->
{error,
{invalid_wire_format,
<<"Invalid boolean element"/utf8>>}}
end
end;
_ ->
{error,
{invalid_wire_format,
<<"Invalid boolean element"/utf8>>}}
end
end.
-file("src/thrifty/reader.gleam", 553).
?DOC(
" Ensure the current recursion `depth` does not exceed configured limits.\n"
"\n"
" Inputs\n"
" - `reader`: the current `types.Reader` containing configured options.\n"
" - `depth`: current recursion depth to validate.\n"
"\n"
" Outputs\n"
" - `Ok(Nil)` when within limits.\n"
" - `Error(types.InvalidWireFormat(\"Exceeded maximum depth\"))` when the depth\n"
" exceeds `options.max_depth`.\n"
).
-spec ensure_depth(thrifty@types:reader(), integer()) -> {ok, nil} |
{error, thrifty@types:decode_error()}.
ensure_depth(Reader, Depth) ->
{reader, _, _, Options} = Reader,
case Depth > erlang:element(2, Options) of
true ->
{error, {invalid_wire_format, <<"Exceeded maximum depth"/utf8>>}};
false ->
{ok, nil}
end.
-file("src/thrifty/reader.gleam", 515).
?DOC(
" Map a Result containing a value and a reader to a Result containing only\n"
" the reader. Utility used by skip helpers to ignore parsed values while\n"
" preserving the updated reader or propagating errors.\n"
).
-spec map_reader(
{ok, {any(), thrifty@types:reader()}} |
{error, thrifty@types:decode_error()}
) -> {ok, thrifty@types:reader()} | {error, thrifty@types:decode_error()}.
map_reader(Result) ->
case Result of
{error, E} ->
{error, E};
{ok, {_, Reader}} ->
{ok, Reader}
end.
-file("src/thrifty/reader.gleam", 482).
?DOC(
" Skip a single element of type `elem_type`.\n"
"\n"
" Inputs\n"
" - `reader`: current `types.Reader` positioned at the element.\n"
" - `elem_type`: element type descriptor.\n"
" - `depth`: current recursion depth.\n"
"\n"
" Outputs\n"
" - `Ok(types.Reader)` positioned after the element on success.\n"
" - `Error(types.DecodeError)` if the element is malformed or limits are exceeded.\n"
).
-spec skip_bytes(thrifty@types:reader(), integer()) -> {ok,
thrifty@types:reader()} |
{error, thrifty@types:decode_error()}.
skip_bytes(Reader, Count) ->
{reader, Data, Byte_pos, Options} = Reader,
case gleam_stdlib:bit_array_slice(Data, Byte_pos, Count) of
{error, _} ->
{error, unexpected_end_of_input};
{ok, _} ->
{ok, set_position(Reader, Byte_pos + Count, Options)}
end.
-file("src/thrifty/reader.gleam", 244).
-spec read_struct_loop(
thrifty@types:reader(),
integer(),
integer(),
list(thrifty@types:field_header())
) -> {ok, {list(thrifty@types:field_header()), thrifty@types:reader()}} |
{error, thrifty@types:decode_error()}.
read_struct_loop(Reader, Depth, Last_field_id, Acc) ->
case thrifty@field:read_field_header(Reader, Last_field_id) of
{error, E} ->
{error, E};
{ok, {{field_header, Field_id, Field_type}, After_header}} ->
case Field_type of
stop ->
{ok, {lists:reverse(Acc), After_header}};
_ ->
case skip_value_at_depth(
After_header,
Field_type,
Depth + 1
) of
{error, E@1} ->
{error, E@1};
{ok, After_value} ->
read_struct_loop(
After_value,
Depth,
Field_id,
[{field_header, Field_id, Field_type} | Acc]
)
end
end
end.
-file("src/thrifty/reader.gleam", 234).
-spec read_struct_at_depth(thrifty@types:reader(), integer()) -> {ok,
{list(thrifty@types:field_header()), thrifty@types:reader()}} |
{error, thrifty@types:decode_error()}.
read_struct_at_depth(Reader, Depth) ->
case ensure_depth(Reader, Depth) of
{error, E} ->
{error, E};
{ok, nil} ->
read_struct_loop(Reader, Depth, 0, [])
end.
-file("src/thrifty/reader.gleam", 380).
?DOC(
" Skip a map container: decode its header, enforce limits, and skip all\n"
" key/value pairs recursively.\n"
"\n"
" Inputs\n"
" - `reader`: current `types.Reader` positioned at a map header.\n"
" - `depth`: current recursion depth.\n"
"\n"
" Outputs\n"
" - `Ok(types.Reader)` with the reader positioned after the map on success.\n"
" - `Error(types.DecodeError)` on malformed input or if configured limits are exceeded.\n"
).
-spec skip_elements(
integer(),
thrifty@container:element_type(),
thrifty@types:reader(),
integer()
) -> {ok, thrifty@types:reader()} | {error, thrifty@types:decode_error()}.
skip_elements(Count, Elem_type, Reader, Depth) ->
case Count of
0 ->
{ok, Reader};
_ ->
case skip_element(Reader, Elem_type, Depth) of
{error, E} ->
{error, E};
{ok, Next_reader} ->
skip_elements(Count - 1, Elem_type, Next_reader, Depth)
end
end.
-file("src/thrifty/reader.gleam", 294).
-spec skip_list_or_set(thrifty@types:reader(), integer()) -> {ok,
thrifty@types:reader()} |
{error, thrifty@types:decode_error()}.
skip_list_or_set(Reader, Depth) ->
case ensure_depth(Reader, Depth) of
{error, E} ->
{error, E};
{ok, nil} ->
{reader, Data, Byte_pos, Options} = Reader,
case thrifty@container:decode_list_header(Data, Byte_pos) of
{error, E@1} ->
{error, E@1};
{ok, {Size, Elem_type, Next_pos}} ->
case ensure_limit(
Size,
erlang:element(3, Options),
<<"Exceeded container size limit"/utf8>>
) of
{error, E@2} ->
{error, E@2};
{ok, nil} ->
skip_elements(
Size,
Elem_type,
set_position(Reader, Next_pos, Options),
Depth + 1
)
end
end
end.
-file("src/thrifty/reader.gleam", 449).
?DOC(
" Skip `count` map entries (key followed by value), returning the reader\n"
" advanced after all entries or an error.\n"
"\n"
" Inputs\n"
" - `count`: number of key/value pairs to skip.\n"
" - `key_type`, `value_type`: element types for keys and values.\n"
" - `reader`: current `types.Reader` positioned at the first key.\n"
" - `depth`: current recursion depth.\n"
"\n"
" Outputs\n"
" - `Ok(types.Reader)` after all entries are skipped.\n"
" - `Error(types.DecodeError)` if any entry fails to skip.\n"
).
-spec skip_element(
thrifty@types:reader(),
thrifty@container:element_type(),
integer()
) -> {ok, thrifty@types:reader()} | {error, thrifty@types:decode_error()}.
skip_element(Reader, Elem_type, Depth) ->
case Elem_type of
bool_type ->
map_reader(read_bool_element(Reader));
i8_type ->
skip_bytes(Reader, 1);
i16_type ->
map_reader(read_i16(Reader));
i32_type ->
map_reader(read_i32(Reader));
i64_type ->
map_reader(read_i64(Reader));
double_type ->
map_reader(read_double(Reader));
binary_type ->
map_reader(read_binary(Reader));
list_type ->
skip_list_or_set(Reader, Depth + 1);
set_type ->
skip_list_or_set(Reader, Depth + 1);
map_type ->
skip_map(Reader, Depth + 1);
struct_type ->
map_reader(read_struct_at_depth(Reader, Depth + 1))
end.
-file("src/thrifty/reader.gleam", 409).
?DOC(
" Skip `count` elements of `elem_type`, returning the reader advanced past\n"
" all elements or an error if any element skip fails.\n"
"\n"
" Inputs\n"
" - `count`: number of elements to skip.\n"
" - `elem_type`: element type descriptor.\n"
" - `reader`: current `types.Reader` positioned at the first element.\n"
" - `depth`: current recursion depth.\n"
"\n"
" Outputs\n"
" - `Ok(types.Reader)` positioned after the skipped elements.\n"
" - `Error(types.DecodeError)` when an element cannot be skipped or limits are exceeded.\n"
).
-spec skip_map_entries(
integer(),
thrifty@container:element_type(),
thrifty@container:element_type(),
thrifty@types:reader(),
integer()
) -> {ok, thrifty@types:reader()} | {error, thrifty@types:decode_error()}.
skip_map_entries(Count, Key_type, Value_type, Reader, Depth) ->
case Count of
0 ->
{ok, Reader};
_ ->
case skip_element(Reader, Key_type, Depth) of
{error, E} ->
{error, E};
{ok, After_key} ->
case skip_element(After_key, Value_type, Depth) of
{error, E@1} ->
{error, E@1};
{ok, After_value} ->
skip_map_entries(
Count - 1,
Key_type,
Value_type,
After_value,
Depth
)
end
end
end.
-file("src/thrifty/reader.gleam", 337).
?DOC(
" Skip a list or set container: decode its header, enforce container limits,\n"
" and skip all contained elements recursively.\n"
"\n"
" Inputs\n"
" - `reader`: current `types.Reader` positioned at a list/set header.\n"
" - `depth`: current recursion depth.\n"
"\n"
" Outputs\n"
" - `Ok(types.Reader)` with the reader positioned after the container on success.\n"
" - `Error(types.DecodeError)` on header decode failures, unexpected end, or if\n"
" container limits are exceeded.\n"
).
-spec skip_map(thrifty@types:reader(), integer()) -> {ok,
thrifty@types:reader()} |
{error, thrifty@types:decode_error()}.
skip_map(Reader, Depth) ->
case ensure_depth(Reader, Depth) of
{error, E} ->
{error, E};
{ok, nil} ->
{reader, Data, Byte_pos, Options} = Reader,
case thrifty@container:decode_map_header(Data, Byte_pos) of
{error, E@1} ->
{error, E@1};
{ok, {Size, Key_type, Value_type, Next_pos}} ->
case ensure_limit(
Size,
erlang:element(3, Options),
<<"Exceeded container size limit"/utf8>>
) of
{error, E@2} ->
{error, E@2};
{ok, nil} ->
skip_map_entries(
Size,
Key_type,
Value_type,
set_position(Reader, Next_pos, Options),
Depth + 1
)
end
end
end.
-file("src/thrifty/reader.gleam", 268).
-spec skip_value_at_depth(
thrifty@types:reader(),
thrifty@types:field_type(),
integer()
) -> {ok, thrifty@types:reader()} | {error, thrifty@types:decode_error()}.
skip_value_at_depth(Reader, Field_type, Depth) ->
case ensure_depth(Reader, Depth) of
{error, E} ->
{error, E};
{ok, nil} ->
case Field_type of
bool_true ->
{ok, Reader};
bool_false ->
{ok, Reader};
stop ->
{ok, Reader};
byte ->
skip_bytes(Reader, 1);
i16 ->
map_reader(read_i16(Reader));
i32 ->
map_reader(read_i32(Reader));
i64 ->
map_reader(read_i64(Reader));
double ->
map_reader(read_double(Reader));
binary ->
map_reader(read_binary(Reader));
list ->
skip_list_or_set(Reader, Depth);
set ->
skip_list_or_set(Reader, Depth);
map ->
skip_map(Reader, Depth);
struct ->
map_reader(read_struct_at_depth(Reader, Depth))
end
end.
-file("src/thrifty/reader.gleam", 221).
?DOC(
" Skip a value of the given `field_type`. Useful when parsing structs and\n"
" encountering unknown/ignored fields. This function enforces depth/size\n"
" limits configured in `ReaderOptions` and returns the advanced reader or an\n"
" error. Example:\n"
"\n"
" let r0 = reader.from_bit_array(some_encoded_list)\n"
" let Ok(r1) = reader.skip_value(r0, types.List)\n"
).
-spec skip_value(thrifty@types:reader(), thrifty@types:field_type()) -> {ok,
thrifty@types:reader()} |
{error, thrifty@types:decode_error()}.
skip_value(Reader, Field_type) ->
skip_value_at_depth(Reader, Field_type, 1).
-file("src/thrifty/reader.gleam", 228).
-spec read_struct(thrifty@types:reader()) -> {ok,
{list(thrifty@types:field_header()), thrifty@types:reader()}} |
{error, thrifty@types:decode_error()}.
read_struct(Reader) ->
read_struct_at_depth(Reader, 1).