blob: 5e2e6dac434196205932d887c4b4e1f28abd0b24 [file]
(*
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.
*)
(* Recursion-depth guard test for the OCaml library (THRIFT-6051).
This exercises the real depth guard that lives in [Thrift.Protocol.t]
(increment_recursion_depth / decrement_recursion_depth, limit 64) through
full struct/union/exception read+write round-trips, NOT by calling those
two methods in isolation.
The recursive types and their read/write functions below are written by
hand to mirror exactly what the OCaml generator now emits (the same
"increment_recursion_depth; Fun.protect ~finally:decrement (fun () -> ...)"
wrapper around every struct reader and writer). Generated code from
test/Recursive.thrift cannot be used directly here for two pre-existing,
unrelated reasons: the generator emits mutually recursive classes
(CoRec/CoUnion/CoError pairs) as separate "class" declarations instead of
"class ... and ...", and TBinaryProtocol.ml does not compile on modern
OCaml (uses removed mutable-string APIs). Both are out of scope for
THRIFT-6051 and flagged separately.
In the OCaml generator unions and exceptions are emitted through the very
same code path as structs (generate_ocaml_struct_definition), so a single
guarded reader/writer template covers all three. The three node types below
make that coverage explicit at runtime. *)
open Thrift
(* Minimal no-op transport: the in-memory protocol below never touches it. *)
class null_transport =
object
inherit Transport.t
method isOpen = true
method opn = ()
method close = ()
method read _buf _off _len = 0
method write _buf _off _len = ()
method flush = ()
end
(* A tiny in-memory protocol that records the structural events as tokens in a
FIFO queue on write and replays them on read. It implements only the methods
the recursive types below use; the rest fail loudly if ever reached. This
stands in for TBinaryProtocol, which does not compile on modern OCaml. *)
type token =
| StructBegin of string
| StructEnd
| FieldBegin of string * Protocol.t_type * int
| FieldEnd
| FieldStop
| I16 of int
class mem_protocol trans =
object (self)
inherit Protocol.t trans
val tokens : token Queue.t = Queue.create ()
method private put t = Queue.add t tokens
method private get = Queue.take tokens
(* functional, structural methods *)
method writeStructBegin name = self#put (StructBegin name)
method writeStructEnd = self#put StructEnd
method writeFieldBegin (name, t, id) = self#put (FieldBegin (name, t, id))
method writeFieldEnd = self#put FieldEnd
method writeFieldStop = self#put FieldStop
method writeI16 i = self#put (I16 i)
method readStructBegin =
(match self#get with StructBegin n -> n | _ -> failwith "expected StructBegin")
method readStructEnd =
(match self#get with StructEnd -> () | _ -> failwith "expected StructEnd")
method readFieldBegin =
(match self#get with
| FieldBegin (n, t, id) -> (n, t, id)
| FieldStop -> ("", Protocol.T_STOP, 0)
| _ -> failwith "expected FieldBegin or FieldStop")
method readFieldEnd =
(match self#get with FieldEnd -> () | _ -> failwith "expected FieldEnd")
method readI16 =
(match self#get with I16 i -> i | _ -> failwith "expected I16")
(* unused by this test *)
method writeMessageBegin _ = failwith "unused"
method writeMessageEnd = failwith "unused"
method writeMapBegin _ = failwith "unused"
method writeMapEnd = failwith "unused"
method writeListBegin _ = failwith "unused"
method writeListEnd = failwith "unused"
method writeSetBegin _ = failwith "unused"
method writeSetEnd = failwith "unused"
method writeBool _ = failwith "unused"
method writeByte _ = failwith "unused"
method writeI32 _ = failwith "unused"
method writeI64 _ = failwith "unused"
method writeDouble _ = failwith "unused"
method writeString _ = failwith "unused"
method writeBinary _ = failwith "unused"
method readMessageBegin = failwith "unused"
method readMessageEnd = failwith "unused"
method readMapBegin = failwith "unused"
method readMapEnd = failwith "unused"
method readListBegin = failwith "unused"
method readListEnd = failwith "unused"
method readSetBegin = failwith "unused"
method readSetEnd = failwith "unused"
method readBool = failwith "unused"
method readByte = failwith "unused"
method readI32 = failwith "unused"
method readI64 = failwith "unused"
method readDouble = failwith "unused"
method readString = failwith "unused"
method readBinary = failwith "unused"
end
let passed = ref 0
let failed = ref 0
let check label cond =
if cond then begin
Printf.printf "PASS: %s\n%!" label;
incr passed
end else begin
Printf.printf "FAIL: %s\n%!" label;
incr failed
end
(* ---- Recursive STRUCT (self-recursive, mirrors generated struct codegen) ---- *)
class rec_struct =
object (_self)
val mutable _other : rec_struct option = None
val mutable _item : int = 0
method get_other = _other
method set_other (x : rec_struct) = _other <- Some x
method set_item i = _item <- i
method write (oprot : Protocol.t) =
oprot#increment_recursion_depth;
Fun.protect ~finally:(fun () -> oprot#decrement_recursion_depth) (fun () ->
oprot#writeStructBegin "RecStruct";
(match _other with None -> () | Some _v ->
oprot#writeFieldBegin ("other", Protocol.T_STRUCT, 1);
_v#write oprot;
oprot#writeFieldEnd);
oprot#writeFieldBegin ("item", Protocol.T_I16, 2);
oprot#writeI16 _item;
oprot#writeFieldEnd;
oprot#writeFieldStop;
oprot#writeStructEnd)
end
let rec read_rec_struct (iprot : Protocol.t) =
let str = new rec_struct in
iprot#increment_recursion_depth;
Fun.protect ~finally:(fun () -> iprot#decrement_recursion_depth) (fun () ->
ignore (iprot#readStructBegin);
(try while true do
let (_, t, id) = iprot#readFieldBegin in
if t = Protocol.T_STOP then raise Break;
(match id with
| 1 -> str#set_other (read_rec_struct iprot)
| 2 -> str#set_item iprot#readI16
| _ -> iprot#skip t);
iprot#readFieldEnd
done with Break -> ());
iprot#readStructEnd);
str
let rec build_struct n =
let s = new rec_struct in
s#set_item n;
if n > 1 then s#set_other (build_struct (n - 1));
s
let rec depth_struct s =
match s#get_other with None -> 1 | Some o -> 1 + depth_struct o
(* Craft an over-limit payload with raw (unguarded) protocol calls, so the
depth limit can only trip on the read side. *)
let rec craft_struct (oprot : Protocol.t) n =
oprot#writeStructBegin "RecStruct";
if n > 1 then begin
oprot#writeFieldBegin ("other", Protocol.T_STRUCT, 1);
craft_struct oprot (n - 1);
oprot#writeFieldEnd
end;
oprot#writeFieldStop;
oprot#writeStructEnd
(* ---- Recursive UNION (OCaml emits unions through the struct path) ---- *)
class rec_union =
object (_self)
val mutable _other : rec_union option = None
method get_other = _other
method set_other (x : rec_union) = _other <- Some x
method write (oprot : Protocol.t) =
oprot#increment_recursion_depth;
Fun.protect ~finally:(fun () -> oprot#decrement_recursion_depth) (fun () ->
oprot#writeStructBegin "RecUnion";
(match _other with None -> () | Some _v ->
oprot#writeFieldBegin ("other", Protocol.T_STRUCT, 1);
_v#write oprot;
oprot#writeFieldEnd);
oprot#writeFieldStop;
oprot#writeStructEnd)
end
let rec read_rec_union (iprot : Protocol.t) =
let str = new rec_union in
iprot#increment_recursion_depth;
Fun.protect ~finally:(fun () -> iprot#decrement_recursion_depth) (fun () ->
ignore (iprot#readStructBegin);
(try while true do
let (_, t, id) = iprot#readFieldBegin in
if t = Protocol.T_STOP then raise Break;
(match id with
| 1 -> str#set_other (read_rec_union iprot)
| _ -> iprot#skip t);
iprot#readFieldEnd
done with Break -> ());
iprot#readStructEnd);
str
let rec build_union n =
let u = new rec_union in
if n > 1 then u#set_other (build_union (n - 1));
u
let rec depth_union u =
match u#get_other with None -> 1 | Some o -> 1 + depth_union o
let rec craft_union (oprot : Protocol.t) n =
oprot#writeStructBegin "RecUnion";
if n > 1 then begin
oprot#writeFieldBegin ("other", Protocol.T_STRUCT, 1);
craft_union oprot (n - 1);
oprot#writeFieldEnd
end;
oprot#writeFieldStop;
oprot#writeStructEnd
(* ---- Recursive EXCEPTION (OCaml emits a class plus an "exception E of cls") ---- *)
class rec_error =
object (_self)
val mutable _other : rec_error option = None
method get_other = _other
method set_other (x : rec_error) = _other <- Some x
method write (oprot : Protocol.t) =
oprot#increment_recursion_depth;
Fun.protect ~finally:(fun () -> oprot#decrement_recursion_depth) (fun () ->
oprot#writeStructBegin "RecError";
(match _other with None -> () | Some _v ->
oprot#writeFieldBegin ("other", Protocol.T_STRUCT, 1);
_v#write oprot;
oprot#writeFieldEnd);
oprot#writeFieldStop;
oprot#writeStructEnd)
end
exception RecError of rec_error
let rec read_rec_error (iprot : Protocol.t) =
let str = new rec_error in
iprot#increment_recursion_depth;
Fun.protect ~finally:(fun () -> iprot#decrement_recursion_depth) (fun () ->
ignore (iprot#readStructBegin);
(try while true do
let (_, t, id) = iprot#readFieldBegin in
if t = Protocol.T_STOP then raise Break;
(match id with
| 1 -> str#set_other (read_rec_error iprot)
| _ -> iprot#skip t);
iprot#readFieldEnd
done with Break -> ());
iprot#readStructEnd);
str
let rec build_error n =
let e = new rec_error in
if n > 1 then e#set_other (build_error (n - 1));
e
let rec depth_error e =
match e#get_other with None -> 1 | Some o -> 1 + depth_error o
let rec craft_error (oprot : Protocol.t) n =
oprot#writeStructBegin "RecError";
if n > 1 then begin
oprot#writeFieldBegin ("other", Protocol.T_STRUCT, 1);
craft_error oprot (n - 1);
oprot#writeFieldEnd
end;
oprot#writeFieldStop;
oprot#writeStructEnd
let limit = 64
let is_depth_limit = function Protocol.E (Protocol.DEPTH_LIMIT, _) -> true | _ -> false
(* Run the full read/write round-trip matrix for one node category. *)
let run_suite label ~build ~write ~read ~depth_of ~craft =
(* 1. A chain exactly at the limit round-trips and preserves its depth. *)
(let proto = new mem_protocol (new null_transport) in
try
write (build limit) proto;
let got = depth_of (read proto) in
check (Printf.sprintf "%s: %d-deep write/read round-trip preserves depth" label limit)
(got = limit)
with e ->
Printf.printf " (unexpected: %s)\n%!" (Printexc.to_string e);
check (Printf.sprintf "%s: %d-deep write/read round-trip preserves depth" label limit) false);
(* 2. Writing one level past the limit raises DEPTH_LIMIT. *)
(let proto = new mem_protocol (new null_transport) in
try
write (build (limit + 1)) proto;
check (Printf.sprintf "%s: write %d-deep raises DEPTH_LIMIT" label (limit + 1)) false
with e ->
check (Printf.sprintf "%s: write %d-deep raises DEPTH_LIMIT" label (limit + 1))
(is_depth_limit e));
(* 3. Reading a crafted over-limit payload raises DEPTH_LIMIT. *)
(let proto = new mem_protocol (new null_transport) in
craft proto (limit + 1);
try
ignore (read proto);
check (Printf.sprintf "%s: read %d-deep raises DEPTH_LIMIT" label (limit + 1)) false
with e ->
check (Printf.sprintf "%s: read %d-deep raises DEPTH_LIMIT" label (limit + 1))
(is_depth_limit e))
(* A payload nested under a field id no reader declares, so every level is
handed to skip(), which descends it on its own. *)
let rec craft_unknown (oprot : Protocol.t) n =
oprot#writeStructBegin "Rec";
if n > 1 then begin
oprot#writeFieldBegin ("unknown", Protocol.T_STRUCT, 99);
craft_unknown oprot (n - 1);
oprot#writeFieldEnd
end;
oprot#writeFieldStop;
oprot#writeStructEnd
let skip_suite () =
(let proto = new mem_protocol (new null_transport) in
craft_unknown proto (limit + 5);
try
ignore (read_rec_struct proto);
check "skip: reading past the limit raises DEPTH_LIMIT" false
with e ->
check "skip: reading past the limit raises DEPTH_LIMIT" (is_depth_limit e));
(let proto = new mem_protocol (new null_transport) in
craft_unknown proto (limit - 2);
try
ignore (read_rec_struct proto);
check "skip: reading within the limit succeeds" true
with _ ->
check "skip: reading within the limit succeeds" false)
let () =
run_suite "struct"
~build:build_struct
~write:(fun o p -> o#write p)
~read:read_rec_struct
~depth_of:depth_struct
~craft:craft_struct;
run_suite "union"
~build:build_union
~write:(fun o p -> o#write p)
~read:read_rec_union
~depth_of:depth_union
~craft:craft_union;
run_suite "exception"
~build:build_error
~write:(fun o p -> o#write p)
~read:read_rec_error
~depth_of:depth_error
~craft:craft_error;
skip_suite ();
(* The exception wrapper itself is exercised so the generated
"exception E of cls" shape is covered as well. *)
(try raise (RecError (build_error 1)) with RecError _ -> check "exception wrapper raises/catches" true);
Printf.printf "\nResults: %d passed, %d failed\n" !passed !failed;
if !failed > 0 then exit 1