This is an automated email from the ASF dual-hosted git repository.

Jens-G pushed a commit to branch release/0.25.0
in repository https://gitbox.apache.org/repos/asf/thrift.git


The following commit(s) were added to refs/heads/release/0.25.0 by this push:
     new 27fa70442 THRIFT-6366: Bound the message size in the Erlang binary and 
compact protocols Client: erl
27fa70442 is described below

commit 27fa70442bccdf981d72b1e57d1080646cb5e360
Author: Jens Geyer <[email protected]>
AuthorDate: Thu Sep 24 00:15:38 2026 +0200

    THRIFT-6366: Bound the message size in the Erlang binary and compact 
protocols
    Client: erl
    
    thrift_binary_protocol and thrift_compact_protocol now read a message only
    up to max_message_size: the option of new/2 or of the protocol factory, or
    else the thrift application's max_message_size (100 MB by default) as it is
    when the protocol is created.
    
    Each read takes its bytes from what the message may still take, before they
    are read, so a string whose declared length would take the message past the
    maximum is refused on its length alone. message_begin sets that budget and
    message_end clears it; between messages each read is held to the maximum on
    its own. A refusal is {error, {message_size_exceeds_maximum, Max}}, which
    message_begin now returns instead of failing a match.
    
    thrift_client_util passes a max_message_size client option on to the
    protocol, and the binary, compact and JSON protocol factories accept it.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
 CHANGES.md                                    |   2 +
 lib/erl/include/thrift_constants.hrl          |   9 +-
 lib/erl/src/thrift_binary_protocol.erl        | 102 +++++++---
 lib/erl/src/thrift_client_util.erl            |   3 +-
 lib/erl/src/thrift_compact_protocol.erl       |  74 +++++--
 lib/erl/src/thrift_json_protocol.erl          |   6 +-
 lib/erl/test/test_thrift_max_message_size.erl | 273 ++++++++++++++++++++++++++
 7 files changed, 419 insertions(+), 50 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index 3a7fb3824..1a9ef214a 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -601,6 +601,8 @@
 
 - [THRIFT-6030](https://issues.apache.org/jira/browse/THRIFT-6030) - Harden 
Erlang protocol negative sizes
 - [THRIFT-6084](https://issues.apache.org/jira/browse/THRIFT-6084) - Add 
byte-count limit to TCompactProtocol varint reader
+- [THRIFT-6366](https://issues.apache.org/jira/browse/THRIFT-6366) - Bound the 
message size in the Erlang binary and compact protocols
+
 
 ### Go
 
diff --git a/lib/erl/include/thrift_constants.hrl 
b/lib/erl/include/thrift_constants.hrl
index 0e46cd727..af55f22d1 100644
--- a/lib/erl/include/thrift_constants.hrl
+++ b/lib/erl/include/thrift_constants.hrl
@@ -75,8 +75,9 @@
 %% bindings use. Override with {thrift, [{max_frame_size, Bytes}]}.
 -define(DEFAULT_MAX_FRAME_SIZE, 16384000).
 
-%% The largest reply thrift_http_transport will read, and the largest message
-%% thrift_json_protocol will read. Same value the other bindings use for their
-%% maximum message size. Override with {thrift, [{max_message_size, Bytes}]},
-%% or for one transport or protocol with its max_message_size option.
+%% The largest message the binary, compact and JSON protocols will read, and
+%% the largest reply thrift_http_transport will read. Same value the other
+%% bindings use for their maximum message size. Override with
+%% {thrift, [{max_message_size, Bytes}]}, or for one transport or protocol with
+%% its max_message_size option.
 -define(DEFAULT_MAX_MESSAGE_SIZE, (100 * 1024 * 1024)).
diff --git a/lib/erl/src/thrift_binary_protocol.erl 
b/lib/erl/src/thrift_binary_protocol.erl
index e94f02521..b1cf57f6b 100644
--- a/lib/erl/src/thrift_binary_protocol.erl
+++ b/lib/erl/src/thrift_binary_protocol.erl
@@ -37,7 +37,11 @@
 -record(binary_protocol, {
     transport :: term(),
     strict_read = true :: boolean(),
-    strict_write = true :: boolean()
+    strict_write = true :: boolean(),
+    % the most a message may take; new/2 settles it
+    max_message_size :: pos_integer() | undefined,
+    % what the message being read may still take; undefined between messages
+    message_bytes_left :: non_neg_integer() | undefined
 }).
 
 -define(VERSION_MASK, 16#FFFF0000).
@@ -47,13 +51,26 @@
 new(Transport) ->
     new(Transport, _Options = []).
 
+%%--------------------------------------------------------------------
+%% Options include:
+%%   {strict_read, Bool}, {strict_write, Bool}
+%%   {max_message_size, Bytes}  = The longest message to read, in place of the
+%%                                thrift application's max_message_size
+%%--------------------------------------------------------------------
 new(Transport, Options) ->
     State = #binary_protocol{transport = Transport},
     State1 = parse_options(Options, State),
-    thrift_protocol:new(?MODULE, State1).
+    thrift_protocol:new(?MODULE, State1#binary_protocol{max_message_size = 
max_message_size(State1)}).
+
+max_message_size(#binary_protocol{max_message_size = undefined}) ->
+    application:get_env(thrift, max_message_size, ?DEFAULT_MAX_MESSAGE_SIZE);
+max_message_size(#binary_protocol{max_message_size = Max}) ->
+    Max.
 
 parse_options([], State) ->
     State;
+parse_options([{max_message_size, Max} | Rest], State) when is_integer(Max), 
Max > 0 ->
+    parse_options(Rest, State#binary_protocol{max_message_size = Max});
 parse_options([{strict_read, Bool} | Rest], State) when is_boolean(Bool) ->
     parse_options(Rest, State#binary_protocol{strict_read = Bool});
 parse_options([{strict_write, Bool} | Rest], State) when is_boolean(Bool) ->
@@ -164,19 +181,17 @@ write(This = #binary_protocol{transport = Trans}, Data) ->
 
 %%
 
-read(This0, message_begin) ->
-    {This1, Initial} = read(This0, ui32),
+read(This0 = #binary_protocol{max_message_size = Max}, message_begin) ->
+    {This1, Initial} = read(This0#binary_protocol{message_bytes_left = Max}, 
ui32),
     case Initial of
         {ok, Sz} when Sz band ?VERSION_MASK =:= ?VERSION_1 ->
             %% we're at version 1
-            {This2, {ok, Name}} = read(This1, string),
-            {This3, {ok, SeqId}} = read(This2, i32),
-            Type = Sz band ?TYPE_MASK,
-            {This3, #protocol_message_begin{
-                name = binary_to_list(Name),
-                type = Type,
-                seqid = SeqId
-            }};
+            case read(This1, string) of
+                {This2, {ok, Name}} ->
+                    read_message_begin(This2, Name, Sz band ?TYPE_MASK);
+                {This2, Error} ->
+                    {This2, Error}
+            end;
         {ok, Sz} when Sz < 0 ->
             %% there's a version number but it's unexpected
             {This1, {error, {bad_binary_protocol_version, Sz}}};
@@ -185,19 +200,20 @@ read(This0, message_begin) ->
             {This1, {error, no_binary_protocol_version}};
         {ok, Sz} when This1#binary_protocol.strict_read =:= false ->
             %% strict_read is false, so just read the old way
-            {This2, {ok, Name}} = read_data(This1, Sz),
-            {This3, {ok, Type}} = read(This2, byte),
-            {This4, {ok, SeqId}} = read(This3, i32),
-            {This4, #protocol_message_begin{
-                name = binary_to_list(Name),
-                type = Type,
-                seqid = SeqId
-            }};
+            case read_data(This1, Sz) of
+                {This2, {ok, Name}} ->
+                    case read(This2, byte) of
+                        {This3, {ok, Type}} -> read_message_begin(This3, Name, 
Type);
+                        {This3, Error} -> {This3, Error}
+                    end;
+                {This2, Error} ->
+                    {This2, Error}
+            end;
         Else ->
             {This1, Else}
     end;
 read(This, message_end) ->
-    {This, ok};
+    {This#binary_protocol{message_bytes_left = undefined}, ok};
 read(This, struct_begin) ->
     {This, ok};
 read(This, struct_end) ->
@@ -304,23 +320,55 @@ read(This0, string) ->
     if Sz < 0 -> error({protocol_error, negative_size}); true -> ok end,
     read_data(This1, Sz).
 
+%% The sequence id that ends a message header, once its name and type are read.
+read_message_begin(This0, Name, Type) ->
+    case read(This0, i32) of
+        {This1, {ok, SeqId}} ->
+            {This1, #protocol_message_begin{
+                name = binary_to_list(Name),
+                type = Type,
+                seqid = SeqId
+            }};
+        {This1, Error} ->
+            {This1, Error}
+    end.
+
 -spec read_data(#binary_protocol{}, non_neg_integer()) ->
     {#binary_protocol{}, {ok, binary()} | {error, _Reason}}.
 read_data(This, 0) ->
     {This, {ok, <<>>}};
-read_data(This = #binary_protocol{transport = Trans}, Len) when 
is_integer(Len) andalso Len > 0 ->
-    {NewTransport, Result} = thrift_transport:read(Trans, Len),
-    {This#binary_protocol{transport = NewTransport}, Result}.
+read_data(This0 = #binary_protocol{transport = Trans}, Len) when 
is_integer(Len) andalso Len > 0 ->
+    case take(This0, Len) of
+        {ok, This1} ->
+            {NewTransport, Result} = thrift_transport:read(Trans, Len),
+            {This1#binary_protocol{transport = NewTransport}, Result};
+        Error ->
+            {This0, Error}
+    end.
+
+%% Takes Len bytes from what the message being read may still take, before
+%% they are read. Between messages, each read is held to the maximum on its 
own.
+take(This = #binary_protocol{message_bytes_left = undefined, max_message_size 
= Max}, Len) when
+    Len =< Max
+->
+    {ok, This};
+take(This = #binary_protocol{message_bytes_left = Left}, Len) when 
is_integer(Left), Len =< Left ->
+    {ok, This#binary_protocol{message_bytes_left = Left - Len}};
+take(#binary_protocol{max_message_size = Max}, _Len) ->
+    {error, {message_size_exceeds_maximum, Max}}.
 
 %%%% FACTORY GENERATION %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
 
 -record(tbp_opts, {
     strict_read = true :: boolean(),
-    strict_write = true :: boolean()
+    strict_write = true :: boolean(),
+    max_message_size :: pos_integer() | undefined
 }).
 
 parse_factory_options([], Opts) ->
     Opts;
+parse_factory_options([{max_message_size, Max} | Rest], Opts) when 
is_integer(Max), Max > 0 ->
+    parse_factory_options(Rest, Opts#tbp_opts{max_message_size = Max});
 parse_factory_options([{strict_read, Bool} | Rest], Opts) when 
is_boolean(Bool) ->
     parse_factory_options(Rest, Opts#tbp_opts{strict_read = Bool});
 parse_factory_options([{strict_write, Bool} | Rest], Opts) when 
is_boolean(Bool) ->
@@ -337,6 +385,10 @@ new_protocol_factory(TransportFactory, Options) ->
                     [
                         {strict_read, ParsedOpts#tbp_opts.strict_read},
                         {strict_write, ParsedOpts#tbp_opts.strict_write}
+                        | [
+                            {max_message_size, Max}
+                         || Max <- [ParsedOpts#tbp_opts.max_message_size], Max 
=/= undefined
+                        ]
                     ]
                 );
             {error, Error} ->
diff --git a/lib/erl/src/thrift_client_util.erl 
b/lib/erl/src/thrift_client_util.erl
index 7c5f094be..447896c04 100644
--- a/lib/erl/src/thrift_client_util.erl
+++ b/lib/erl/src/thrift_client_util.erl
@@ -41,7 +41,8 @@ split_options([], ProtoIn, TransIn) ->
 split_options([Opt = {OptKey, _} | Rest], ProtoIn, TransIn) when
     OptKey =:= strict_read;
     OptKey =:= strict_write;
-    OptKey =:= protocol
+    OptKey =:= protocol;
+    OptKey =:= max_message_size
 ->
     split_options(Rest, [Opt | ProtoIn], TransIn);
 split_options([Opt = {OptKey, _} | Rest], ProtoIn, TransIn) when
diff --git a/lib/erl/src/thrift_compact_protocol.erl 
b/lib/erl/src/thrift_compact_protocol.erl
index 5b3fec054..70464e1f6 100644
--- a/lib/erl/src/thrift_compact_protocol.erl
+++ b/lib/erl/src/thrift_compact_protocol.erl
@@ -46,7 +46,11 @@
     read_stack = [] :: iodata(),
     read_value = ?CBOOL_NONE :: cbool(),
     write_stack = [] :: iodata(),
-    write_id = ?ID_NONE :: non_neg_integer()
+    write_id = ?ID_NONE :: non_neg_integer(),
+    % the most a message may take; new/2 settles it
+    max_message_size :: pos_integer() | undefined,
+    % what the message being read may still take; undefined between messages
+    message_bytes_left :: non_neg_integer() | undefined
 }).
 
 -type t_compact() :: #t_compact{}.
@@ -91,10 +95,26 @@ cbool_to_bool(Value) -> Value =:= ?CBOOL_TRUE.
 
 new(Transport) -> new(Transport, _Options = []).
 
-new(Transport, _Options) ->
-    State = #t_compact{transport = Transport},
+%%--------------------------------------------------------------------
+%% Options include:
+%%   {max_message_size, Bytes}  = The longest message to read, in place of the
+%%                                thrift application's max_message_size
+%%--------------------------------------------------------------------
+new(Transport, Options) ->
+    Max = proplists:get_value(max_message_size, 
max_message_size_options(Options)),
+    State = #t_compact{transport = Transport, max_message_size = 
max_message_size(Max)},
     thrift_protocol:new(?MODULE, State).
 
+max_message_size_options(Options) when is_list(Options) ->
+    [Opt || Opt = {max_message_size, Max} <- Options, is_integer(Max), Max > 
0];
+max_message_size_options(_Options) ->
+    [].
+
+max_message_size(undefined) ->
+    application:get_env(thrift, max_message_size, ?DEFAULT_MAX_MESSAGE_SIZE);
+max_message_size(Max) ->
+    Max.
+
 flush_transport(This = #t_compact{transport = Transport}) ->
     {NewTransport, Result} = thrift_transport:flush(Transport),
     {This#t_compact{transport = NewTransport}, Result}.
@@ -246,19 +266,23 @@ write(This = #t_compact{transport = Trans}, Data) ->
 %%
 %%
 
-read(This0, message_begin) ->
-    {This1, {ok, ?PROTOCOL_ID}} = read(This0, ubyte),
+read(This0 = #t_compact{max_message_size = Max}, message_begin) ->
+    {This1, {ok, ?PROTOCOL_ID}} = read(This0#t_compact{message_bytes_left = 
Max}, ubyte),
     {This2, {ok, VerAndType}} = read(This1, ubyte),
     ?VERSION_1 = VerAndType band ?VERSION_MASK,
     {This3, {ok, SeqId}} = read(This2, ui32),
-    {This4, {ok, Name}} = read(This3, string),
-    {This4, #protocol_message_begin{
-        name = binary_to_list(Name),
-        type = (VerAndType bsr ?TYPE_SHIFT_AMOUNT) band ?TYPE_BITS,
-        seqid = SeqId
-    }};
+    case read(This3, string) of
+        {This4, {ok, Name}} ->
+            {This4, #protocol_message_begin{
+                name = binary_to_list(Name),
+                type = (VerAndType bsr ?TYPE_SHIFT_AMOUNT) band ?TYPE_BITS,
+                seqid = SeqId
+            }};
+        {This4, Error} ->
+            {This4, Error}
+    end;
 read(This, message_end) ->
-    {This, ok};
+    {This#t_compact{message_bytes_left = undefined}, ok};
 read(This = #t_compact{read_stack = Stack}, struct_begin) ->
     {This#t_compact{read_stack = [0 | Stack]}, ok};
 read(This = #t_compact{read_stack = [_H | T]}, struct_end) ->
@@ -381,20 +405,36 @@ read(This0, string) ->
     {#t_compact{}, {ok, binary()} | {error, _Reason}}.
 read_data(This, 0) ->
     {This, {ok, <<>>}};
-read_data(This = #t_compact{transport = Trans}, Len) when is_integer(Len) 
andalso Len > 0 ->
-    {NewTransport, Result} = thrift_transport:read(Trans, Len),
-    {This#t_compact{transport = NewTransport}, Result}.
+read_data(This0 = #t_compact{transport = Trans}, Len) when is_integer(Len) 
andalso Len > 0 ->
+    case take(This0, Len) of
+        {ok, This1} ->
+            {NewTransport, Result} = thrift_transport:read(Trans, Len),
+            {This1#t_compact{transport = NewTransport}, Result};
+        Error ->
+            {This0, Error}
+    end.
+
+%% Takes Len bytes from what the message being read may still take, before
+%% they are read. Between messages, each read is held to the maximum on its 
own.
+take(This = #t_compact{message_bytes_left = undefined, max_message_size = 
Max}, Len) when
+    Len =< Max
+->
+    {ok, This};
+take(This = #t_compact{message_bytes_left = Left}, Len) when is_integer(Left), 
Len =< Left ->
+    {ok, This#t_compact{message_bytes_left = Left - Len}};
+take(#t_compact{max_message_size = Max}, _Len) ->
+    {error, {message_size_exceeds_maximum, Max}}.
 
 %%%% FACTORY GENERATION %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
 
 %% returns a (fun() -> thrift_protocol())
-new_protocol_factory(TransportFactory, _Options) ->
+new_protocol_factory(TransportFactory, Options) ->
     F = fun() ->
         case TransportFactory() of
             {ok, Transport} ->
                 thrift_compact_protocol:new(
                     Transport,
-                    []
+                    max_message_size_options(Options)
                 );
             {error, Error} ->
                 {error, Error}
diff --git a/lib/erl/src/thrift_json_protocol.erl 
b/lib/erl/src/thrift_json_protocol.erl
index d2602949c..c5331db55 100644
--- a/lib/erl/src/thrift_json_protocol.erl
+++ b/lib/erl/src/thrift_json_protocol.erl
@@ -717,10 +717,10 @@ key_to_number(Key, Convert, Error) ->
 %%%% FACTORY GENERATION %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
 
 %% returns a (fun() -> thrift_protocol())
-new_protocol_factory(TransportFactory, _Options) ->
-    % Only strice read/write are implemented
+new_protocol_factory(TransportFactory, Options) ->
+    MaxOptions = [Opt || is_list(Options), Opt = {max_message_size, _} <- 
Options],
     F = fun() ->
         {ok, Transport} = TransportFactory(),
-        thrift_json_protocol:new(Transport, [])
+        thrift_json_protocol:new(Transport, MaxOptions)
     end,
     {ok, F}.
diff --git a/lib/erl/test/test_thrift_max_message_size.erl 
b/lib/erl/test/test_thrift_max_message_size.erl
new file mode 100644
index 000000000..d9364ab02
--- /dev/null
+++ b/lib/erl/test/test_thrift_max_message_size.erl
@@ -0,0 +1,273 @@
+%%
+%% Licensed to the Apache Software Foundation (ASF) under one
+%% or more contributor license agreements. See the NOTICE file
+%% distributed with this work for additional information
+%% regarding copyright ownership. The ASF licenses this file
+%% to you under the Apache License, Version 2.0 (the
+%% "License"); you may not use this file except in compliance
+%% with the License. You may obtain a copy of the License at
+%%
+%%   http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing,
+%% software distributed under the License is distributed on an
+%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+%% KIND, either express or implied. See the License for the
+%% specific language governing permissions and limitations
+%% under the License.
+%%
+
+%% The binary and compact protocols read a message only up to its maximum
+%% size: the max_message_size option, or else the thrift application's
+%% max_message_size.
+-module(test_thrift_max_message_size).
+-include_lib("eunit/include/eunit.hrl").
+-include("thrift_constants.hrl").
+-include("thrift_protocol.hrl").
+
+%% thrift_socket_server handler callbacks
+-export([handle_function/2, handle_error/2]).
+
+-define(ARGS, {struct, [{1, string}]}).
+-define(MAX, 4096).
+
+%% {Name, Module, Options} of each protocol and header form under test.
+protocols() ->
+    [
+        {"binary", thrift_binary_protocol, []},
+        {"binary without version header", thrift_binary_protocol, [
+            {strict_read, false}, {strict_write, false}
+        ]},
+        {"compact", thrift_compact_protocol, []}
+    ].
+
+new(Module, Bytes, Options) ->
+    {ok, Transport} = thrift_membuffer_transport:new(Bytes),
+    {ok, Protocol} = Module:new(Transport, Options),
+    Protocol.
+
+%% What is left in the memory buffer under Protocol, or under the protocol's
+%% own state, which a failed read inside the protocol raises with.
+buffered({protocol, _Module, State}) ->
+    buffered(State);
+buffered(State) ->
+    {t_transport, thrift_membuffer_transport, {t_membuffer, Bytes}} = 
element(2, State),
+    Bytes.
+
+%% The bytes of a call named Name whose one argument is the string Text.
+message(Module, Options, Name, Text) ->
+    Protocol0 = new(Module, <<>>, Options),
+    {Protocol1, ok} = thrift_protocol:write(Protocol0, #protocol_message_begin{
+        name = Name, type = ?tMessageType_CALL, seqid = 1
+    }),
+    {Protocol2, ok} = thrift_protocol:write(Protocol1, {?ARGS, {args, Text}}),
+    {Protocol3, ok} = thrift_protocol:write(Protocol2, message_end),
+    {Protocol4, ok} = thrift_protocol:flush_transport(Protocol3),
+    iolist_to_binary(buffered(Protocol4)).
+
+%% A call of exactly Size bytes, the argument making up the rest. The compact
+%% protocol writes the string's length in as few bytes as it needs, so the
+%% first guess is corrected once.
+message_of_size(Module, Options, Size) ->
+    Guess = Size - byte_size(message(Module, Options, "m", <<>>)),
+    First = message(Module, Options, "m", binary:copy(<<"a">>, Guess)),
+    Bytes = message(Module, Options, "m", binary:copy(<<"a">>, Guess - 
(byte_size(First) - Size))),
+    Size = byte_size(Bytes),
+    Bytes.
+
+%% Reads one call as thrift_processor does: {Result, bytes taken from the
+%% transport, Protocol}. A refusal inside the arguments surfaces as the
+%% exception the struct reader raises for any failed read; where that
+%% exception does not carry the protocol, the bytes taken are unknown.
+read_call(Protocol0, Bytes) ->
+    try read_whole_call(Protocol0) of
+        {Outcome, Protocol} ->
+            {Outcome, byte_size(Bytes) - iolist_size(buffered(Protocol)), 
Protocol}
+    catch
+        error:{badmatch, {Protocol, {error, _} = Error}} ->
+            {Error, byte_size(Bytes) - iolist_size(buffered(Protocol)), 
Protocol};
+        error:{case_clause, {error, _} = Error} ->
+            {Error, unknown, undefined}
+    end.
+
+read_whole_call(Protocol0) ->
+    case thrift_protocol:read(Protocol0, message_begin) of
+        {Protocol1, #protocol_message_begin{name = Name}} ->
+            {Protocol2, {ok, Args}} = thrift_protocol:read(Protocol1, ?ARGS),
+            {Protocol3, ok} = thrift_protocol:read(Protocol2, message_end),
+            {{ok, Name, Args}, Protocol3};
+        {Protocol1, Error} ->
+            {Error, Protocol1}
+    end.
+
+refused() -> {error, {message_size_exceeds_maximum, ?MAX}}.
+
+%% A call up to the maximum is read, and one a byte longer is refused. A
+%% string that would take the call past the maximum is refused from its
+%% length, before its bytes are taken from the transport.
+limit_test_() ->
+    [
+        {Name,
+            ?_test(begin
+                Options = [{max_message_size, ?MAX} | Base],
+                AtMax = message_of_size(Module, Base, ?MAX),
+                ?assertMatch(
+                    {{ok, "m", _}, ?MAX, _}, read_call(new(Module, AtMax, 
Options), AtMax)
+                ),
+                ByOne = message_of_size(Module, Base, ?MAX + 1),
+                ?assertMatch({{error, _}, _, _}, read_call(new(Module, ByOne, 
Options), ByOne)),
+                Long = message_of_size(Module, Base, 2 * ?MAX),
+                {Result, Taken, _} = read_call(new(Module, Long, Options), 
Long),
+                ?assertEqual(refused(), Result),
+                ?assert(Taken < ?MAX)
+            end)}
+     || {Name, Module, Base} <- protocols()
+    ].
+
+%% A message name longer than the maximum is refused by message_begin, which
+%% reports it as an error rather than raising, and before its bytes are taken.
+name_test_() ->
+    [
+        {Name,
+            ?_test(begin
+                Bytes = message(Module, Base, 
binary_to_list(binary:copy(<<"n">>, ?MAX)), <<>>),
+                Protocol = new(Module, Bytes, [{max_message_size, ?MAX} | 
Base]),
+                {Protocol1, Result} = thrift_protocol:read(Protocol, 
message_begin),
+                ?assertEqual(refused(), Result),
+                ?assert(byte_size(Bytes) - iolist_size(buffered(Protocol1)) =< 
8)
+            end)}
+     || {Name, Module, Base} <- protocols()
+    ].
+
+%% Each message has the whole maximum to itself.
+one_message_after_another_test_() ->
+    [
+        {Name,
+            ?_test(begin
+                One = message_of_size(Module, Base, ?MAX),
+                Bytes = <<One/binary, One/binary, One/binary>>,
+                Protocol0 = new(Module, Bytes, [{max_message_size, ?MAX} | 
Base]),
+                {{ok, _, _}, _, Protocol1} = read_call(Protocol0, Bytes),
+                {{ok, _, _}, _, Protocol2} = read_call(Protocol1, Bytes),
+                ?assertMatch({{ok, "m", _}, _, _}, read_call(Protocol2, Bytes))
+            end)}
+     || {Name, Module, Base} <- protocols()
+    ].
+
+%% Outside a message, reads are held to the maximum one at a time and do not
+%% add up: many structs read one after another without a message around them.
+outside_a_message_test_() ->
+    [
+        {Name,
+            ?_test(begin
+                Protocol0 = new(Module, <<>>, Base),
+                {Protocol1, ok} = thrift_protocol:write(
+                    Protocol0, {?ARGS, {args, binary:copy(<<"s">>, 1000)}}
+                ),
+                One = iolist_to_binary(buffered(Protocol1)),
+                Bytes = binary:copy(One, 10),
+                Protocol2 = new(Module, Bytes, [{max_message_size, ?MAX} | 
Base]),
+                Final = lists:foldl(
+                    fun(_, P) ->
+                        {P1, {ok, {Text}}} = thrift_protocol:read(P, ?ARGS),
+                        ?assertEqual(1000, byte_size(Text)),
+                        P1
+                    end,
+                    Protocol2,
+                    lists:seq(1, 10)
+                ),
+                ?assertEqual(0, iolist_size(buffered(Final)))
+            end)}
+     || {Name, Module, Base} <- protocols()
+    ].
+
+%% Without the option, the thrift application's max_message_size applies; the
+%% option takes its place.
+application_env_test_() ->
+    {setup,
+        fun() ->
+            ok = load_thrift_application(),
+            Saved = application:get_env(thrift, max_message_size),
+            ok = application:set_env(thrift, max_message_size, ?MAX),
+            Saved
+        end,
+        fun restore_env/1, [
+            {Name,
+                ?_test(begin
+                    Over = message_of_size(Module, Base, ?MAX + 1),
+                    ?assertMatch({{error, _}, _, _}, read_call(new(Module, 
Over, Base), Over)),
+                    ?assertMatch(
+                        {{ok, "m", _}, _, _},
+                        read_call(new(Module, Over, [{max_message_size, 2 * 
?MAX} | Base]), Over)
+                    )
+                end)}
+         || {Name, Module, Base} <- protocols()
+        ]}.
+
+%%%% Over a socket %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+handle_function(_Function, Args) -> {reply, element(1, Args)}.
+
+handle_error(_Function, _Reason) -> ok.
+
+%% The server refuses a call over the application's maximum and closes that
+%% connection, and goes on serving others. A client given max_message_size
+%% refuses a reply over it.
+socket_test_() ->
+    {setup,
+        fun() ->
+            ok = load_thrift_application(),
+            Saved = application:get_env(thrift, max_message_size),
+            ok = application:set_env(thrift, max_message_size, 64 * 1024),
+            Saved
+        end,
+        fun restore_env/1, [
+            {atom_to_list(Protocol), ?_test(socket_calls(Port, Protocol))}
+         || {Port, Protocol} <- [{9110, binary}, {9111, compact}]
+        ]}.
+
+socket_calls(Port, Protocol) ->
+    {ok, Server} = thrift_socket_server:start([
+        {ip, "127.0.0.1"},
+        {port, Port},
+        {name, list_to_atom("max_message_size_server_" ++ 
integer_to_list(Port))},
+        {service, thrift_test_thrift},
+        {handler, ?MODULE},
+        {protocol, Protocol}
+    ]),
+    try
+        Options = [{protocol, Protocol}, {recv_timeout, 2000}],
+        {ok, Client0} = thrift_client_util:new("127.0.0.1", Port, 
thrift_test_thrift, Options),
+        {Client1, Small} = thrift_client:call(Client0, testString, 
[<<"small">>]),
+        ?assertEqual({ok, <<"small">>}, Small),
+        Large = binary:copy(<<"L">>, 128 * 1024),
+        ?assertMatch({_, {error, _}}, catch_call(Client1, testString, 
[Large])),
+        {ok, Client2} = thrift_client_util:new("127.0.0.1", Port, 
thrift_test_thrift, Options),
+        ?assertMatch(
+            {_, {ok, <<"again">>}}, thrift_client:call(Client2, testString, 
[<<"again">>])
+        ),
+        {ok, Client3} = thrift_client_util:new(
+            "127.0.0.1", Port, thrift_test_thrift, [{max_message_size, 1024} | 
Options]
+        ),
+        ?assertMatch({_, {error, _}}, catch_call(Client3, testString, 
[binary:copy(<<"r">>, 2048)]))
+    after
+        thrift_socket_server:stop(Server)
+    end.
+
+%% A call whose connection fails or whose reply is refused, as {error, _}.
+catch_call(Client, Function, Args) ->
+    try thrift_client:call(Client, Function, Args) of
+        {Client1, {ok, Reply}} -> {Client1, {ok, Reply}};
+        {Client1, Other} -> {Client1, {error, Other}}
+    catch
+        Class:Reason -> {Client, {error, {Class, Reason}}}
+    end.
+
+load_thrift_application() ->
+    case application:load(thrift) of
+        ok -> ok;
+        {error, {already_loaded, thrift}} -> ok
+    end.
+
+restore_env(undefined) -> application:unset_env(thrift, max_message_size);
+restore_env({ok, Value}) -> application:set_env(thrift, max_message_size, 
Value).

Reply via email to