···11+; Fuzz testing with Crowbar
22+;
33+; Quick check (runs all tests with random inputs):
44+; dune build @fuzz
55+; -- or --
66+; dune exec fuzz/fuzz.exe
77+;
88+; With AFL instrumentation for thorough fuzzing:
99+; dune build --profile=afl @fuzz-afl # build the fuzzer
1010+; dune build --profile=afl @run-afl # run afl-fuzz (interactive)
1111+;
1212+; Note: AFL profile requires an OCaml compiler with AFL support:
1313+; opam switch create ./afl ocaml-variants.5.2.0+options ocaml-option-afl
1414+1515+(executable
1616+ (name fuzz)
1717+ (libraries crowbar yamlrw)
1818+ (modules
1919+ fuzz
2020+ fuzz_common
2121+ fuzz_encoding
2222+ fuzz_chomping
2323+ fuzz_tag
2424+ fuzz_value
2525+ fuzz_yamlrw
2626+ fuzz_emitter))
2727+2828+; Standalone AFL fuzzer for targeted parser testing
2929+; This is a simpler executable that directly reads input and exercises the parser
3030+; Best used with AFL instrumentation for finding parser bugs
3131+3232+(executable
3333+ (name fuzz_afl)
3434+ (libraries yamlrw)
3535+ (modules fuzz_afl))
3636+3737+; Alias to run Crowbar fuzz tests (quick check mode)
3838+(rule
3939+ (alias fuzz)
4040+ (deps
4141+ (source_tree input))
4242+ (action
4343+ (run %{exe:fuzz.exe})))
4444+4545+; Alias to build AFL-instrumented fuzzer
4646+; Use with: dune build --profile=afl @fuzz-afl
4747+(rule
4848+ (alias fuzz-afl)
4949+ (deps
5050+ (source_tree input)
5151+ fuzz_afl.exe)
5252+ (action
5353+ (echo "AFL fuzzer built. To run: dune exec --profile=afl @run-afl\n")))
5454+5555+; Alias to run AFL fuzzer
5656+; Use with: dune build --profile=afl @run-afl
5757+; Set AFL_TIMEOUT to control duration in seconds (default: 300 = 5 minutes)
5858+; Example: AFL_TIMEOUT=3600 dune build --profile=afl @run-afl # 1 hour
5959+(rule
6060+ (alias run-afl)
6161+ (deps
6262+ (source_tree input)
6363+ fuzz_afl.exe)
6464+ (action
6565+ (setenv AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES 1
6666+ (setenv AFL_SKIP_CPUFREQ 1
6767+ (run afl-fuzz -V %{env:AFL_TIMEOUT=300} -m none -i input -o %{workspace_root}/_fuzz -- ./%{exe:fuzz_afl.exe} @@)))))
+36
ocaml-yamlrw/fuzz/fuzz.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Main entry point for fuzz tests.
77+88+ Run without arguments for Crowbar's default mode (quick check):
99+ {[
1010+ dune exec fuzz/fuzz.exe
1111+ ]}
1212+1313+ Run with AFL for thorough fuzzing:
1414+ {[
1515+ mkdir -p fuzz/input
1616+ echo -n "" > fuzz/input/empty
1717+ echo "key: value" > fuzz/input/simple
1818+ echo -e "- a\n- b\n- c" > fuzz/input/list
1919+ afl-fuzz -m none -i fuzz/input -o _fuzz -- _build/default/fuzz/fuzz.exe @@
2020+ ]}
2121+2222+ For AFL mode, build with afl-instrument:
2323+ {[
2424+ opam install crowbar afl-persistent
2525+ dune build fuzz/fuzz.exe
2626+ ]} *)
2727+2828+(* Force linking of all fuzz test modules via side effects *)
2929+let () =
3030+ Fuzz_common.run ();
3131+ Fuzz_encoding.run ();
3232+ Fuzz_chomping.run ();
3333+ Fuzz_tag.run ();
3434+ Fuzz_value.run ();
3535+ Fuzz_yamlrw.run ();
3636+ Fuzz_emitter.run ()
+114
ocaml-yamlrw/fuzz/fuzz_afl.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** AFL-specific fuzzer for yamlrw parser.
77+88+ This is a standalone AFL fuzzer that reads input from a file or stdin
99+ and exercises the parser. Build with afl-instrument for best results.
1010+1111+ Usage:
1212+ {[
1313+ # Build with AFL instrumentation
1414+ opam switch create . ocaml-variants.5.2.0+options ocaml-option-afl
1515+ dune build fuzz/fuzz_afl.exe
1616+1717+ # Create seed corpus
1818+ mkdir -p fuzz/input
1919+ echo -n "" > fuzz/input/empty
2020+ echo "null" > fuzz/input/null
2121+ echo "true" > fuzz/input/bool
2222+ echo "42" > fuzz/input/int
2323+ echo "3.14" > fuzz/input/float
2424+ echo "hello" > fuzz/input/string
2525+ echo "key: value" > fuzz/input/mapping
2626+ echo -e "- a\n- b" > fuzz/input/sequence
2727+ echo -e "---\nfoo\n..." > fuzz/input/document
2828+ echo "&anchor value" > fuzz/input/anchor
2929+ echo "!tag value" > fuzz/input/tag
3030+ echo -e "|\n literal\n block" > fuzz/input/literal
3131+ echo -e ">\n folded\n block" > fuzz/input/folded
3232+ echo "'single quoted'" > fuzz/input/single
3333+ echo '"double quoted"' > fuzz/input/double
3434+3535+ # Run AFL
3636+ afl-fuzz -m none -i fuzz/input -o _fuzz -- _build/default/fuzz/fuzz_afl.exe @@
3737+ ]} *)
3838+3939+(** Read entire file as string *)
4040+let read_file filename =
4141+ let ic = open_in_bin filename in
4242+ let n = in_channel_length ic in
4343+ let s = really_input_string ic n in
4444+ close_in ic;
4545+ s
4646+4747+(** Read from stdin until EOF *)
4848+let read_stdin () =
4949+ let buf = Buffer.create 1024 in
5050+ try
5151+ while true do
5252+ Buffer.add_channel buf stdin 1024
5353+ done;
5454+ assert false
5555+ with End_of_file -> Buffer.contents buf
5656+5757+(** Fuzz target: exercises all major parsing paths *)
5858+let fuzz_target input =
5959+ (* Test value parsing *)
6060+ (try
6161+ let v = Yamlrw.of_string input in
6262+ (* Exercise serialization *)
6363+ let _ = Yamlrw.to_string v in
6464+ (* Exercise different styles *)
6565+ let _ = Yamlrw.to_string ~layout_style:`Block v in
6666+ let _ = Yamlrw.to_string ~layout_style:`Flow v in
6767+ (* Exercise pp *)
6868+ let _ = Format.asprintf "%a" Yamlrw.pp v in
6969+ ()
7070+ with Yamlrw.Yamlrw_error _ -> ());
7171+7272+ (* Test yaml parsing (with alias resolution) *)
7373+ (try
7474+ let y = Yamlrw.yaml_of_string ~resolve_aliases:true input in
7575+ let _ = Yamlrw.yaml_to_string y in
7676+ ()
7777+ with Yamlrw.Yamlrw_error _ -> ());
7878+7979+ (* Test yaml parsing (without alias resolution) *)
8080+ (try
8181+ let y = Yamlrw.yaml_of_string ~resolve_aliases:false input in
8282+ let _ = Yamlrw.yaml_to_string y in
8383+ ()
8484+ with Yamlrw.Yamlrw_error _ -> ());
8585+8686+ (* Test document parsing *)
8787+ (try
8888+ let docs = Yamlrw.documents_of_string input in
8989+ let _ = Yamlrw.documents_to_string docs in
9090+ ()
9191+ with Yamlrw.Yamlrw_error _ -> ());
9292+9393+ (* Test encoding detection *)
9494+ let enc, _ = Yamlrw.Encoding.detect input in
9595+ let _ = Yamlrw.Encoding.to_string enc in
9696+9797+ (* Test streaming parser *)
9898+ (try
9999+ let parser = Yamlrw.Stream.parser input in
100100+ Yamlrw.Stream.iter (fun _ _ _ -> ()) parser
101101+ with Yamlrw.Yamlrw_error _ -> ());
102102+103103+ (* Test scanner directly *)
104104+ (try
105105+ let scanner = Yamlrw.Scanner.of_string input in
106106+ let _ = Yamlrw.Scanner.to_list scanner in
107107+ ()
108108+ with Yamlrw.Yamlrw_error _ -> ())
109109+110110+let () =
111111+ let input =
112112+ if Array.length Sys.argv > 1 then read_file Sys.argv.(1) else read_stdin ()
113113+ in
114114+ fuzz_target input
+93
ocaml-yamlrw/fuzz/fuzz_chomping.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Fuzz tests for Chomping module *)
77+88+open Crowbar
99+1010+(** Test of_char/to_char roundtrip for valid chars *)
1111+let () =
1212+ add_test ~name:"chomping: of_char/to_char roundtrip" [ uint8 ] @@ fun n ->
1313+ let c = Char.chr n in
1414+ match Yamlrw.Chomping.of_char c with
1515+ | None -> check true (* Invalid char, that's fine *)
1616+ | Some chomping -> (
1717+ match Yamlrw.Chomping.to_char chomping with
1818+ | None ->
1919+ (* Clip has no char representation *)
2020+ if chomping <> Yamlrw.Chomping.Clip then
2121+ fail "non-Clip chomping should have char"
2222+ else check true
2323+ | Some c' ->
2424+ if c <> c' then fail "roundtrip mismatch"
2525+ else check true)
2626+2727+(** Test that to_string never crashes *)
2828+let () =
2929+ add_test ~name:"chomping: to_string Strip" [ const () ] @@ fun () ->
3030+ let _ = Yamlrw.Chomping.to_string Yamlrw.Chomping.Strip in
3131+ check true
3232+3333+let () =
3434+ add_test ~name:"chomping: to_string Clip" [ const () ] @@ fun () ->
3535+ let _ = Yamlrw.Chomping.to_string Yamlrw.Chomping.Clip in
3636+ check true
3737+3838+let () =
3939+ add_test ~name:"chomping: to_string Keep" [ const () ] @@ fun () ->
4040+ let _ = Yamlrw.Chomping.to_string Yamlrw.Chomping.Keep in
4141+ check true
4242+4343+(** Test pp never crashes *)
4444+let () =
4545+ add_test ~name:"chomping: pp" [ range 3 ] @@ fun n ->
4646+ let chomping =
4747+ match n with
4848+ | 0 -> Yamlrw.Chomping.Strip
4949+ | 1 -> Yamlrw.Chomping.Clip
5050+ | _ -> Yamlrw.Chomping.Keep
5151+ in
5252+ let _ = Format.asprintf "%a" Yamlrw.Chomping.pp chomping in
5353+ check true
5454+5555+(** Test equality is reflexive *)
5656+let () =
5757+ add_test ~name:"chomping: equal reflexive" [ range 3 ] @@ fun n ->
5858+ let chomping =
5959+ match n with
6060+ | 0 -> Yamlrw.Chomping.Strip
6161+ | 1 -> Yamlrw.Chomping.Clip
6262+ | _ -> Yamlrw.Chomping.Keep
6363+ in
6464+ if not (Yamlrw.Chomping.equal chomping chomping) then
6565+ fail "chomping not equal to itself"
6666+ else check true
6767+6868+(** Test specific valid indicators *)
6969+let () =
7070+ add_test ~name:"chomping: strip indicator '-'" [ const () ] @@ fun () ->
7171+ match Yamlrw.Chomping.of_char '-' with
7272+ | Some Yamlrw.Chomping.Strip -> check true
7373+ | _ -> fail "'-' should parse as Strip"
7474+7575+let () =
7676+ add_test ~name:"chomping: keep indicator '+'" [ const () ] @@ fun () ->
7777+ match Yamlrw.Chomping.of_char '+' with
7878+ | Some Yamlrw.Chomping.Keep -> check true
7979+ | _ -> fail "'+' should parse as Keep"
8080+8181+(** Test invalid chars return None *)
8282+let () =
8383+ add_test ~name:"chomping: invalid chars" [ const () ] @@ fun () ->
8484+ let invalid_chars = [ 'a'; 'z'; '0'; '9'; ' '; '\n'; '#' ] in
8585+ List.iter
8686+ (fun c ->
8787+ match Yamlrw.Chomping.of_char c with
8888+ | None -> ()
8989+ | Some _ -> fail (Printf.sprintf "char '%c' should not be valid" c))
9090+ invalid_chars;
9191+ check true
9292+9393+let run () = ()
+55
ocaml-yamlrw/fuzz/fuzz_common.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Common utilities for fuzz tests. *)
77+88+open Crowbar
99+1010+let to_bytes buf =
1111+ let len = String.length buf in
1212+ let b = Bytes.create len in
1313+ Bytes.blit_string buf 0 b 0 len;
1414+ b
1515+1616+(** Generator for printable ASCII strings (useful for YAML content) *)
1717+let printable_char = map [ range 95 ] (fun n -> Char.chr (n + 32))
1818+1919+let printable_string =
2020+ map [ list printable_char ] (fun chars ->
2121+ String.init (List.length chars) (List.nth chars))
2222+2323+(** Generator for valid YAML scalar content (excludes problematic chars) *)
2424+let yaml_safe_char =
2525+ map [ range 94 ] (fun n ->
2626+ let c = n + 32 in
2727+ (* Skip colon, hash, and other YAML special chars at start *)
2828+ if c = 58 (* : *) || c = 35 (* # *) then Char.chr 97 (* 'a' *)
2929+ else Char.chr c)
3030+3131+let yaml_safe_string =
3232+ map [ list yaml_safe_char ] (fun chars ->
3333+ String.init (List.length chars) (List.nth chars))
3434+3535+(** Generator for identifier-like strings *)
3636+let ident_char =
3737+ map [ range 62 ] (fun n ->
3838+ if n < 26 then Char.chr (n + 97) (* a-z *)
3939+ else if n < 52 then Char.chr (n - 26 + 65) (* A-Z *)
4040+ else if n < 62 then Char.chr (n - 52 + 48) (* 0-9 *)
4141+ else '_')
4242+4343+let ident_string =
4444+ map [ list1 ident_char ] (fun chars ->
4545+ String.init (List.length chars) (List.nth chars))
4646+4747+(** Catch exceptions and pass the test if expected exception occurs *)
4848+let catch_invalid_arg f =
4949+ try f () with Invalid_argument _ -> check true
5050+5151+let catch_yamlrw_error f =
5252+ try f ()
5353+ with Yamlrw.Yamlrw_error _ -> check true
5454+5555+let run () = ()
+283
ocaml-yamlrw/fuzz/fuzz_emitter.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Fuzz tests for the Emitter module - test random event sequences *)
77+88+open Crowbar
99+open Fuzz_common
1010+1111+(** Event type for fuzzing *)
1212+type fuzz_event =
1313+ | Stream_start
1414+ | Stream_end
1515+ | Doc_start
1616+ | Doc_end
1717+ | Scalar of string
1818+ | Alias of string
1919+ | Seq_start
2020+ | Seq_end
2121+ | Map_start
2222+ | Map_end
2323+2424+(** Generator for fuzz events *)
2525+let fuzz_event =
2626+ choose
2727+ [
2828+ const Stream_start;
2929+ const Stream_end;
3030+ const Doc_start;
3131+ const Doc_end;
3232+ map [ ident_string ] (fun s -> Scalar s);
3333+ map [ ident_string ] (fun s -> Alias s);
3434+ const Seq_start;
3535+ const Seq_end;
3636+ const Map_start;
3737+ const Map_end;
3838+ ]
3939+4040+(** Emit a fuzz event to an emitter - may fail with Yamlrw_error *)
4141+let emit_fuzz_event emitter = function
4242+ | Stream_start -> Yamlrw.Stream.stream_start emitter `Utf8
4343+ | Stream_end -> Yamlrw.Stream.stream_end emitter
4444+ | Doc_start -> Yamlrw.Stream.document_start emitter ()
4545+ | Doc_end -> Yamlrw.Stream.document_end emitter ()
4646+ | Scalar s -> Yamlrw.Stream.scalar emitter s
4747+ | Alias s -> Yamlrw.Stream.alias emitter s
4848+ | Seq_start -> Yamlrw.Stream.sequence_start emitter ()
4949+ | Seq_end -> Yamlrw.Stream.sequence_end emitter
5050+ | Map_start -> Yamlrw.Stream.mapping_start emitter ()
5151+ | Map_end -> Yamlrw.Stream.mapping_end emitter
5252+5353+(** Test that random event sequences don't crash the emitter *)
5454+let () =
5555+ add_test ~name:"emitter: random events crash safety" [ list fuzz_event ]
5656+ @@ fun events ->
5757+ let emitter = Yamlrw.Stream.emitter () in
5858+ List.iter
5959+ (fun ev ->
6060+ try emit_fuzz_event emitter ev with Yamlrw.Yamlrw_error _ -> ())
6161+ events;
6262+ check true
6363+6464+(** Test that valid event sequences produce parseable output *)
6565+let () =
6666+ add_test ~name:"emitter: valid sequence roundtrip" [ list ident_string ]
6767+ @@ fun items ->
6868+ if List.length items > 0 then begin
6969+ let emitter = Yamlrw.Stream.emitter () in
7070+ (try
7171+ Yamlrw.Stream.stream_start emitter `Utf8;
7272+ Yamlrw.Stream.document_start emitter ();
7373+ Yamlrw.Stream.sequence_start emitter ();
7474+ List.iter (fun s -> Yamlrw.Stream.scalar emitter s) items;
7575+ Yamlrw.Stream.sequence_end emitter;
7676+ Yamlrw.Stream.document_end emitter ();
7777+ Yamlrw.Stream.stream_end emitter;
7878+ let yaml = Yamlrw.Stream.contents emitter in
7979+ (* Try to parse the emitted YAML *)
8080+ let _ = Yamlrw.of_string yaml in
8181+ ()
8282+ with Yamlrw.Yamlrw_error _ -> ());
8383+ check true
8484+ end
8585+ else check true
8686+8787+(** Test that valid mapping event sequences produce parseable output *)
8888+let () =
8989+ add_test ~name:"emitter: valid mapping roundtrip"
9090+ [ list (pair ident_string ident_string) ]
9191+ @@ fun pairs ->
9292+ if List.length pairs > 0 then begin
9393+ let emitter = Yamlrw.Stream.emitter () in
9494+ (try
9595+ Yamlrw.Stream.stream_start emitter `Utf8;
9696+ Yamlrw.Stream.document_start emitter ();
9797+ Yamlrw.Stream.mapping_start emitter ();
9898+ List.iter
9999+ (fun (k, v) ->
100100+ Yamlrw.Stream.scalar emitter k;
101101+ Yamlrw.Stream.scalar emitter v)
102102+ pairs;
103103+ Yamlrw.Stream.mapping_end emitter;
104104+ Yamlrw.Stream.document_end emitter ();
105105+ Yamlrw.Stream.stream_end emitter;
106106+ let yaml = Yamlrw.Stream.contents emitter in
107107+ (* Try to parse the emitted YAML *)
108108+ let _ = Yamlrw.of_string yaml in
109109+ ()
110110+ with Yamlrw.Yamlrw_error _ -> ());
111111+ check true
112112+ end
113113+ else check true
114114+115115+(** Test nested sequences *)
116116+let () =
117117+ add_test ~name:"emitter: nested sequences" [ range 10; list ident_string ]
118118+ @@ fun depth items ->
119119+ if depth > 0 && List.length items > 0 then begin
120120+ let emitter = Yamlrw.Stream.emitter () in
121121+ (try
122122+ Yamlrw.Stream.stream_start emitter `Utf8;
123123+ Yamlrw.Stream.document_start emitter ();
124124+ for _ = 1 to depth do
125125+ Yamlrw.Stream.sequence_start emitter ()
126126+ done;
127127+ List.iter (fun s -> Yamlrw.Stream.scalar emitter s) items;
128128+ for _ = 1 to depth do
129129+ Yamlrw.Stream.sequence_end emitter
130130+ done;
131131+ Yamlrw.Stream.document_end emitter ();
132132+ Yamlrw.Stream.stream_end emitter;
133133+ let yaml = Yamlrw.Stream.contents emitter in
134134+ let _ = Yamlrw.of_string yaml in
135135+ ()
136136+ with Yamlrw.Yamlrw_error _ -> ());
137137+ check true
138138+ end
139139+ else check true
140140+141141+(** Test nested mappings *)
142142+let () =
143143+ add_test ~name:"emitter: nested mappings" [ range 10; ident_string ]
144144+ @@ fun depth value ->
145145+ if depth > 0 && String.length value > 0 then begin
146146+ let emitter = Yamlrw.Stream.emitter () in
147147+ (try
148148+ Yamlrw.Stream.stream_start emitter `Utf8;
149149+ Yamlrw.Stream.document_start emitter ();
150150+ for i = 1 to depth do
151151+ Yamlrw.Stream.mapping_start emitter ();
152152+ Yamlrw.Stream.scalar emitter (Printf.sprintf "key%d" i)
153153+ done;
154154+ Yamlrw.Stream.scalar emitter value;
155155+ for _ = 1 to depth do
156156+ Yamlrw.Stream.mapping_end emitter
157157+ done;
158158+ Yamlrw.Stream.document_end emitter ();
159159+ Yamlrw.Stream.stream_end emitter;
160160+ let yaml = Yamlrw.Stream.contents emitter in
161161+ let _ = Yamlrw.of_string yaml in
162162+ ()
163163+ with Yamlrw.Yamlrw_error _ -> ());
164164+ check true
165165+ end
166166+ else check true
167167+168168+(** Test emitter with different scalar styles *)
169169+let () =
170170+ add_test ~name:"emitter: scalar styles" [ printable_string ] @@ fun s ->
171171+ let styles =
172172+ [ `Any; `Plain; `Single_quoted; `Double_quoted; `Literal; `Folded ]
173173+ in
174174+ List.iter
175175+ (fun style ->
176176+ let emitter = Yamlrw.Stream.emitter () in
177177+ (try
178178+ Yamlrw.Stream.stream_start emitter `Utf8;
179179+ Yamlrw.Stream.document_start emitter ();
180180+ Yamlrw.Stream.scalar emitter ~style s;
181181+ Yamlrw.Stream.document_end emitter ();
182182+ Yamlrw.Stream.stream_end emitter;
183183+ let yaml = Yamlrw.Stream.contents emitter in
184184+ let _ = Yamlrw.of_string yaml in
185185+ ()
186186+ with Yamlrw.Yamlrw_error _ -> ()))
187187+ styles;
188188+ check true
189189+190190+(** Test emitter with anchors and aliases *)
191191+let () =
192192+ add_test ~name:"emitter: anchors and aliases" [ ident_string; ident_string ]
193193+ @@ fun anchor value ->
194194+ if String.length anchor > 0 && String.length value > 0 then begin
195195+ let emitter = Yamlrw.Stream.emitter () in
196196+ (try
197197+ Yamlrw.Stream.stream_start emitter `Utf8;
198198+ Yamlrw.Stream.document_start emitter ();
199199+ Yamlrw.Stream.mapping_start emitter ();
200200+ Yamlrw.Stream.scalar emitter "original";
201201+ Yamlrw.Stream.scalar emitter ~anchor value;
202202+ Yamlrw.Stream.scalar emitter "reference";
203203+ Yamlrw.Stream.alias emitter anchor;
204204+ Yamlrw.Stream.mapping_end emitter;
205205+ Yamlrw.Stream.document_end emitter ();
206206+ Yamlrw.Stream.stream_end emitter;
207207+ let yaml = Yamlrw.Stream.contents emitter in
208208+ let _ = Yamlrw.of_string yaml in
209209+ ()
210210+ with Yamlrw.Yamlrw_error _ -> ());
211211+ check true
212212+ end
213213+ else check true
214214+215215+(** Test emitter with tags *)
216216+let () =
217217+ add_test ~name:"emitter: tagged scalars" [ ident_string; ident_string ]
218218+ @@ fun tag value ->
219219+ if String.length value > 0 then begin
220220+ let emitter = Yamlrw.Stream.emitter () in
221221+ (try
222222+ Yamlrw.Stream.stream_start emitter `Utf8;
223223+ Yamlrw.Stream.document_start emitter ();
224224+ Yamlrw.Stream.scalar emitter ~tag:("!" ^ tag) value;
225225+ Yamlrw.Stream.document_end emitter ();
226226+ Yamlrw.Stream.stream_end emitter;
227227+ let yaml = Yamlrw.Stream.contents emitter in
228228+ let _ = Yamlrw.yaml_of_string yaml in
229229+ ()
230230+ with Yamlrw.Yamlrw_error _ -> ());
231231+ check true
232232+ end
233233+ else check true
234234+235235+(** Test emitter with layout styles *)
236236+let () =
237237+ add_test ~name:"emitter: layout styles" [ list ident_string ] @@ fun items ->
238238+ if List.length items > 0 then begin
239239+ let styles = [ `Any; `Block; `Flow ] in
240240+ List.iter
241241+ (fun style ->
242242+ let emitter = Yamlrw.Stream.emitter () in
243243+ (try
244244+ Yamlrw.Stream.stream_start emitter `Utf8;
245245+ Yamlrw.Stream.document_start emitter ();
246246+ Yamlrw.Stream.sequence_start emitter ~style ();
247247+ List.iter (fun s -> Yamlrw.Stream.scalar emitter s) items;
248248+ Yamlrw.Stream.sequence_end emitter;
249249+ Yamlrw.Stream.document_end emitter ();
250250+ Yamlrw.Stream.stream_end emitter;
251251+ let yaml = Yamlrw.Stream.contents emitter in
252252+ let _ = Yamlrw.of_string yaml in
253253+ ()
254254+ with Yamlrw.Yamlrw_error _ -> ()))
255255+ styles;
256256+ check true
257257+ end
258258+ else check true
259259+260260+(** Test multiple documents *)
261261+let () =
262262+ add_test ~name:"emitter: multiple documents" [ range 5; ident_string ]
263263+ @@ fun count value ->
264264+ if count > 0 && String.length value > 0 then begin
265265+ let emitter = Yamlrw.Stream.emitter () in
266266+ (try
267267+ Yamlrw.Stream.stream_start emitter `Utf8;
268268+ for i = 1 to count do
269269+ Yamlrw.Stream.document_start emitter ();
270270+ Yamlrw.Stream.scalar emitter (Printf.sprintf "%s%d" value i);
271271+ Yamlrw.Stream.document_end emitter ()
272272+ done;
273273+ Yamlrw.Stream.stream_end emitter;
274274+ let yaml = Yamlrw.Stream.contents emitter in
275275+ let docs = Yamlrw.documents_of_string yaml in
276276+ if List.length docs <> count then fail "document count mismatch"
277277+ else ()
278278+ with Yamlrw.Yamlrw_error _ -> ());
279279+ check true
280280+ end
281281+ else check true
282282+283283+let run () = ()
+79
ocaml-yamlrw/fuzz/fuzz_encoding.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Fuzz tests for Encoding module *)
77+88+open Crowbar
99+1010+(** Test that encoding detection never crashes on arbitrary input *)
1111+let () =
1212+ add_test ~name:"encoding: detect crash safety" [ bytes ] @@ fun buf ->
1313+ let _ = Yamlrw.Encoding.detect buf in
1414+ check true
1515+1616+(** Test that to_string never crashes for any detected encoding *)
1717+let () =
1818+ add_test ~name:"encoding: to_string after detect" [ bytes ] @@ fun buf ->
1919+ let enc, _ = Yamlrw.Encoding.detect buf in
2020+ let _ = Yamlrw.Encoding.to_string enc in
2121+ check true
2222+2323+(** Test that pp never crashes *)
2424+let () =
2525+ add_test ~name:"encoding: pp after detect" [ bytes ] @@ fun buf ->
2626+ let enc, _ = Yamlrw.Encoding.detect buf in
2727+ let _ = Format.asprintf "%a" Yamlrw.Encoding.pp enc in
2828+ check true
2929+3030+(** Test encoding equality is reflexive *)
3131+let () =
3232+ add_test ~name:"encoding: equal reflexive" [ bytes ] @@ fun buf ->
3333+ let enc, _ = Yamlrw.Encoding.detect buf in
3434+ if not (Yamlrw.Encoding.equal enc enc) then fail "encoding not equal to itself"
3535+ else check true
3636+3737+(** Test that BOM length is always non-negative and reasonable *)
3838+let () =
3939+ add_test ~name:"encoding: bom_length non-negative" [ bytes ] @@ fun buf ->
4040+ let _, bom_len = Yamlrw.Encoding.detect buf in
4141+ if bom_len < 0 then fail "negative BOM length"
4242+ else if bom_len > 4 then fail "BOM length too large (max 4 for UTF-32)"
4343+ else check true
4444+4545+(** Test specific BOM patterns *)
4646+let () =
4747+ add_test ~name:"encoding: UTF-8 BOM" [ const () ] @@ fun () ->
4848+ let utf8_bom = "\xEF\xBB\xBF" in
4949+ let enc, len = Yamlrw.Encoding.detect utf8_bom in
5050+ if enc <> `Utf8 then fail "expected UTF-8"
5151+ else if len <> 3 then fail "expected BOM length 3"
5252+ else check true
5353+5454+let () =
5555+ add_test ~name:"encoding: UTF-16 BE BOM" [ const () ] @@ fun () ->
5656+ let utf16be_bom = "\xFE\xFF" in
5757+ let enc, len = Yamlrw.Encoding.detect utf16be_bom in
5858+ if enc <> `Utf16be then fail "expected UTF-16 BE"
5959+ else if len <> 2 then fail "expected BOM length 2"
6060+ else check true
6161+6262+let () =
6363+ add_test ~name:"encoding: UTF-16 LE BOM" [ const () ] @@ fun () ->
6464+ (* Use BOM followed by non-null bytes to avoid ambiguity with UTF-32 LE *)
6565+ let utf16le_bom = "\xFF\xFEab" in
6666+ let enc, len = Yamlrw.Encoding.detect utf16le_bom in
6767+ if enc <> `Utf16le then fail "expected UTF-16 LE"
6868+ else if len <> 2 then fail "expected BOM length 2"
6969+ else check true
7070+7171+let () =
7272+ add_test ~name:"encoding: empty string defaults to UTF-8" [ const () ]
7373+ @@ fun () ->
7474+ let enc, len = Yamlrw.Encoding.detect "" in
7575+ if enc <> `Utf8 then fail "expected UTF-8 for empty string"
7676+ else if len <> 0 then fail "expected BOM length 0 for empty string"
7777+ else check true
7878+7979+let run () = ()
+137
ocaml-yamlrw/fuzz/fuzz_tag.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Fuzz tests for Tag module *)
77+88+open Crowbar
99+open Fuzz_common
1010+1111+(** Test that of_string never crashes on arbitrary input *)
1212+let () =
1313+ add_test ~name:"tag: of_string crash safety" [ bytes ] @@ fun buf ->
1414+ let _ = Yamlrw.Tag.of_string buf in
1515+ check true
1616+1717+(** Test of_string/to_string roundtrip *)
1818+let () =
1919+ add_test ~name:"tag: of_string/to_string roundtrip" [ bytes ] @@ fun buf ->
2020+ match Yamlrw.Tag.of_string buf with
2121+ | None -> check true (* Invalid tag, that's fine *)
2222+ | Some tag ->
2323+ let s = Yamlrw.Tag.to_string tag in
2424+ (* Re-parse should succeed *)
2525+ (match Yamlrw.Tag.of_string s with
2626+ | None -> fail "re-parse of to_string output failed"
2727+ | Some tag' ->
2828+ if not (Yamlrw.Tag.equal tag tag') then fail "roundtrip mismatch"
2929+ else check true)
3030+3131+(** Test to_uri never crashes for valid tags *)
3232+let () =
3333+ add_test ~name:"tag: to_uri after of_string" [ bytes ] @@ fun buf ->
3434+ match Yamlrw.Tag.of_string buf with
3535+ | None -> check true
3636+ | Some tag ->
3737+ let _ = Yamlrw.Tag.to_uri tag in
3838+ check true
3939+4040+(** Test pp never crashes *)
4141+let () =
4242+ add_test ~name:"tag: pp" [ bytes ] @@ fun buf ->
4343+ match Yamlrw.Tag.of_string buf with
4444+ | None -> check true
4545+ | Some tag ->
4646+ let _ = Format.asprintf "%a" Yamlrw.Tag.pp tag in
4747+ check true
4848+4949+(** Test equality is reflexive *)
5050+let () =
5151+ add_test ~name:"tag: equal reflexive" [ bytes ] @@ fun buf ->
5252+ match Yamlrw.Tag.of_string buf with
5353+ | None -> check true
5454+ | Some tag ->
5555+ if not (Yamlrw.Tag.equal tag tag) then fail "tag not equal to itself"
5656+ else check true
5757+5858+(** Test compare is antisymmetric *)
5959+let () =
6060+ add_test ~name:"tag: compare antisymmetric" [ bytes; bytes ]
6161+ @@ fun buf1 buf2 ->
6262+ match (Yamlrw.Tag.of_string buf1, Yamlrw.Tag.of_string buf2) with
6363+ | Some t1, Some t2 ->
6464+ let cmp1 = Yamlrw.Tag.compare t1 t2 in
6565+ let cmp2 = Yamlrw.Tag.compare t2 t1 in
6666+ if cmp1 > 0 && cmp2 >= 0 then fail "compare not antisymmetric"
6767+ else if cmp1 < 0 && cmp2 <= 0 then fail "compare not antisymmetric"
6868+ else if cmp1 = 0 && cmp2 <> 0 then fail "compare not antisymmetric"
6969+ else check true
7070+ | _ -> check true
7171+7272+(** Test make function *)
7373+let () =
7474+ add_test ~name:"tag: make" [ ident_string; ident_string ]
7575+ @@ fun handle suffix ->
7676+ let tag = Yamlrw.Tag.make ~handle ~suffix in
7777+ let _ = Yamlrw.Tag.to_string tag in
7878+ let _ = Yamlrw.Tag.to_uri tag in
7979+ check true
8080+8181+(** Test standard tags exist and have expected properties *)
8282+let () =
8383+ add_test ~name:"tag: standard tags" [ const () ] @@ fun () ->
8484+ let tags =
8585+ [
8686+ (Yamlrw.Tag.null, Yamlrw.Tag.is_null);
8787+ (Yamlrw.Tag.bool, Yamlrw.Tag.is_bool);
8888+ (Yamlrw.Tag.int, Yamlrw.Tag.is_int);
8989+ (Yamlrw.Tag.float, Yamlrw.Tag.is_float);
9090+ (Yamlrw.Tag.str, Yamlrw.Tag.is_str);
9191+ (Yamlrw.Tag.seq, Yamlrw.Tag.is_seq);
9292+ (Yamlrw.Tag.map, Yamlrw.Tag.is_map);
9393+ ]
9494+ in
9595+ List.iter
9696+ (fun (tag, pred) ->
9797+ if not (pred tag) then fail "standard tag predicate failed"
9898+ else
9999+ let _ = Yamlrw.Tag.to_string tag in
100100+ let _ = Yamlrw.Tag.to_uri tag in
101101+ ())
102102+ tags;
103103+ check true
104104+105105+(** Test tag predicates are mutually exclusive for standard tags *)
106106+let () =
107107+ add_test ~name:"tag: predicates mutually exclusive" [ const () ] @@ fun () ->
108108+ let tags =
109109+ [
110110+ Yamlrw.Tag.null;
111111+ Yamlrw.Tag.bool;
112112+ Yamlrw.Tag.int;
113113+ Yamlrw.Tag.float;
114114+ Yamlrw.Tag.str;
115115+ Yamlrw.Tag.seq;
116116+ Yamlrw.Tag.map;
117117+ ]
118118+ in
119119+ let predicates =
120120+ [
121121+ Yamlrw.Tag.is_null;
122122+ Yamlrw.Tag.is_bool;
123123+ Yamlrw.Tag.is_int;
124124+ Yamlrw.Tag.is_float;
125125+ Yamlrw.Tag.is_str;
126126+ Yamlrw.Tag.is_seq;
127127+ Yamlrw.Tag.is_map;
128128+ ]
129129+ in
130130+ List.iter
131131+ (fun tag ->
132132+ let count = List.fold_left (fun n p -> if p tag then n + 1 else n) 0 predicates in
133133+ if count <> 1 then fail "tag matched multiple predicates")
134134+ tags;
135135+ check true
136136+137137+let run () = ()
+390
ocaml-yamlrw/fuzz/fuzz_value.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Fuzz tests for Value module *)
77+88+open Crowbar
99+open Fuzz_common
1010+1111+(** Generator for Value.t *)
1212+let rec value_gen depth =
1313+ if depth <= 0 then
1414+ choose
1515+ [
1616+ const `Null;
1717+ map [ bool ] (fun b -> `Bool b);
1818+ map [ float ] (fun f -> `Float f);
1919+ map [ printable_string ] (fun s -> `String s);
2020+ ]
2121+ else
2222+ choose
2323+ [
2424+ const `Null;
2525+ map [ bool ] (fun b -> `Bool b);
2626+ map [ float ] (fun f -> `Float f);
2727+ map [ printable_string ] (fun s -> `String s);
2828+ map [ list (value_gen (depth - 1)) ] (fun vs -> `A vs);
2929+ map
3030+ [ list (pair ident_string (value_gen (depth - 1))) ]
3131+ (fun pairs -> `O pairs);
3232+ ]
3333+3434+let value = value_gen 3
3535+3636+(** Test pp never crashes *)
3737+let () =
3838+ add_test ~name:"value: pp" [ value ] @@ fun v ->
3939+ let _ = Format.asprintf "%a" Yamlrw.Value.pp v in
4040+ check true
4141+4242+(** Test equal is reflexive *)
4343+let () =
4444+ add_test ~name:"value: equal reflexive" [ value ] @@ fun v ->
4545+ if not (Yamlrw.Value.equal v v) then fail "value not equal to itself"
4646+ else check true
4747+4848+(** Test compare is reflexive (returns 0 for same value) *)
4949+let () =
5050+ add_test ~name:"value: compare reflexive" [ value ] @@ fun v ->
5151+ if Yamlrw.Value.compare v v <> 0 then fail "compare should return 0 for same value"
5252+ else check true
5353+5454+(** Test type_name never crashes *)
5555+let () =
5656+ add_test ~name:"value: type_name" [ value ] @@ fun v ->
5757+ let _ = Yamlrw.Value.type_name v in
5858+ check true
5959+6060+(** Test safe accessors return correct types *)
6161+let () =
6262+ add_test ~name:"value: as_null" [ value ] @@ fun v ->
6363+ (match (v, Yamlrw.Value.as_null v) with
6464+ | `Null, Some () -> ()
6565+ | `Null, None -> fail "as_null should return Some for Null"
6666+ | _, Some () -> fail "as_null should return None for non-Null"
6767+ | _, None -> ());
6868+ check true
6969+7070+let () =
7171+ add_test ~name:"value: as_bool" [ value ] @@ fun v ->
7272+ (match (v, Yamlrw.Value.as_bool v) with
7373+ | `Bool b, Some b' when b = b' -> ()
7474+ | `Bool _, Some _ -> fail "as_bool returned wrong value"
7575+ | `Bool _, None -> fail "as_bool should return Some for Bool"
7676+ | _, Some _ -> fail "as_bool should return None for non-Bool"
7777+ | _, None -> ());
7878+ check true
7979+8080+let () =
8181+ add_test ~name:"value: as_float" [ value ] @@ fun v ->
8282+ (match (v, Yamlrw.Value.as_float v) with
8383+ | `Float f, Some f' when f = f' || (Float.is_nan f && Float.is_nan f') -> ()
8484+ | `Float _, Some _ -> fail "as_float returned wrong value"
8585+ | `Float _, None -> fail "as_float should return Some for Float"
8686+ | _, Some _ -> fail "as_float should return None for non-Float"
8787+ | _, None -> ());
8888+ check true
8989+9090+let () =
9191+ add_test ~name:"value: as_string" [ value ] @@ fun v ->
9292+ (match (v, Yamlrw.Value.as_string v) with
9393+ | `String s, Some s' when s = s' -> ()
9494+ | `String _, Some _ -> fail "as_string returned wrong value"
9595+ | `String _, None -> fail "as_string should return Some for String"
9696+ | _, Some _ -> fail "as_string should return None for non-String"
9797+ | _, None -> ());
9898+ check true
9999+100100+let () =
101101+ add_test ~name:"value: as_list" [ value ] @@ fun v ->
102102+ (match (v, Yamlrw.Value.as_list v) with
103103+ | `A lst, Some lst' when lst = lst' -> ()
104104+ | `A _, Some _ -> fail "as_list returned wrong value"
105105+ | `A _, None -> fail "as_list should return Some for A"
106106+ | _, Some _ -> fail "as_list should return None for non-A"
107107+ | _, None -> ());
108108+ check true
109109+110110+let () =
111111+ add_test ~name:"value: as_assoc" [ value ] @@ fun v ->
112112+ (match (v, Yamlrw.Value.as_assoc v) with
113113+ | `O pairs, Some pairs' when pairs = pairs' -> ()
114114+ | `O _, Some _ -> fail "as_assoc returned wrong value"
115115+ | `O _, None -> fail "as_assoc should return Some for O"
116116+ | _, Some _ -> fail "as_assoc should return None for non-O"
117117+ | _, None -> ());
118118+ check true
119119+120120+(** Test constructors *)
121121+let () =
122122+ add_test ~name:"value: null constructor" [ const () ] @@ fun () ->
123123+ if Yamlrw.Value.null <> `Null then fail "null should be `Null"
124124+ else check true
125125+126126+let () =
127127+ add_test ~name:"value: bool constructor" [ bool ] @@ fun b ->
128128+ if Yamlrw.Value.bool b <> `Bool b then fail "bool constructor mismatch"
129129+ else check true
130130+131131+let () =
132132+ add_test ~name:"value: int constructor" [ range 1000000 ] @@ fun n ->
133133+ (* Use smaller range since floats can't exactly represent all int64 values *)
134134+ match Yamlrw.Value.int n with
135135+ | `Float f when Float.to_int f = n -> check true
136136+ | `Float _ -> fail "int constructor roundtrip failed"
137137+ | _ -> fail "int should produce Float"
138138+139139+let () =
140140+ add_test ~name:"value: float constructor" [ float ] @@ fun f ->
141141+ match Yamlrw.Value.float f with
142142+ | `Float f' when f = f' || (Float.is_nan f && Float.is_nan f') -> check true
143143+ | `Float _ -> fail "float constructor roundtrip failed"
144144+ | _ -> fail "float should produce Float"
145145+146146+let () =
147147+ add_test ~name:"value: string constructor" [ printable_string ] @@ fun s ->
148148+ if Yamlrw.Value.string s <> `String s then fail "string constructor mismatch"
149149+ else check true
150150+151151+(** Test object operations *)
152152+let () =
153153+ add_test ~name:"value: mem/find consistency" [ value; ident_string ]
154154+ @@ fun v key ->
155155+ match v with
156156+ | `O _ ->
157157+ let has_key = Yamlrw.Value.mem key v in
158158+ let found = Yamlrw.Value.find key v in
159159+ if has_key && Option.is_none found then fail "mem true but find None"
160160+ else if (not has_key) && Option.is_some found then
161161+ fail "mem false but find Some"
162162+ else check true
163163+ | _ -> check true
164164+165165+(** Test map preserves structure *)
166166+let () =
167167+ add_test ~name:"value: map preserves list length" [ value ] @@ fun v ->
168168+ match v with
169169+ | `A lst ->
170170+ let mapped = Yamlrw.Value.map (fun x -> x) v in
171171+ (match mapped with
172172+ | `A lst' when List.length lst = List.length lst' -> check true
173173+ | `A _ -> fail "map changed list length"
174174+ | _ -> fail "map changed type")
175175+ | _ -> check true
176176+177177+(** Test combine for objects *)
178178+let () =
179179+ add_test ~name:"value: combine objects" [ value; value ] @@ fun v1 v2 ->
180180+ match (v1, v2) with
181181+ | `O pairs1, `O pairs2 ->
182182+ let combined = Yamlrw.Value.combine v1 v2 in
183183+ (match combined with
184184+ | `O pairs ->
185185+ (* Combined should have all keys from both *)
186186+ let keys1 = List.map fst pairs1 in
187187+ let keys2 = List.map fst pairs2 in
188188+ let all_keys =
189189+ List.sort_uniq String.compare (keys1 @ keys2)
190190+ in
191191+ let combined_keys =
192192+ List.sort_uniq String.compare (List.map fst pairs)
193193+ in
194194+ if all_keys = combined_keys then check true
195195+ else fail "combine missing keys"
196196+ | _ -> fail "combine should produce object")
197197+ | _ -> check true
198198+199199+(** Test generated value -> serialize -> parse roundtrip *)
200200+let () =
201201+ add_test ~name:"value: generated value roundtrip" [ value ] @@ fun v ->
202202+ (try
203203+ let s = Yamlrw.to_string v in
204204+ let v' = Yamlrw.of_string s in
205205+ if not (Yamlrw.equal v v') then fail "generated value roundtrip mismatch"
206206+ else ()
207207+ with Yamlrw.Yamlrw_error _ ->
208208+ (* Some generated values might not roundtrip perfectly due to YAML ambiguities *)
209209+ ());
210210+ check true
211211+212212+(** Test generated value serialization with block style *)
213213+let () =
214214+ add_test ~name:"value: generated block style" [ value ] @@ fun v ->
215215+ (try
216216+ let s = Yamlrw.to_string ~layout_style:`Block v in
217217+ let _ = Yamlrw.of_string s in
218218+ ()
219219+ with Yamlrw.Yamlrw_error _ -> ());
220220+ check true
221221+222222+(** Test generated value serialization with flow style *)
223223+let () =
224224+ add_test ~name:"value: generated flow style" [ value ] @@ fun v ->
225225+ (try
226226+ let s = Yamlrw.to_string ~layout_style:`Flow v in
227227+ let _ = Yamlrw.of_string s in
228228+ ()
229229+ with Yamlrw.Yamlrw_error _ -> ());
230230+ check true
231231+232232+(** Test to_json/of_json roundtrip for generated values *)
233233+let () =
234234+ add_test ~name:"value: to_json/of_json generated" [ value ] @@ fun v ->
235235+ let y = Yamlrw.of_json v in
236236+ let v' = Yamlrw.to_json y in
237237+ if not (Yamlrw.equal v v') then fail "to_json/of_json roundtrip mismatch"
238238+ else check true
239239+240240+(** Test compare is transitive *)
241241+let () =
242242+ add_test ~name:"value: compare transitive" [ value; value; value ]
243243+ @@ fun v1 v2 v3 ->
244244+ let c12 = Yamlrw.Value.compare v1 v2 in
245245+ let c23 = Yamlrw.Value.compare v2 v3 in
246246+ let c13 = Yamlrw.Value.compare v1 v3 in
247247+ (* If v1 <= v2 and v2 <= v3 then v1 <= v3 *)
248248+ if c12 <= 0 && c23 <= 0 && c13 > 0 then fail "compare not transitive"
249249+ else if c12 >= 0 && c23 >= 0 && c13 < 0 then fail "compare not transitive"
250250+ else check true
251251+252252+(** Test equal is symmetric *)
253253+let () =
254254+ add_test ~name:"value: equal symmetric" [ value; value ] @@ fun v1 v2 ->
255255+ let eq12 = Yamlrw.Value.equal v1 v2 in
256256+ let eq21 = Yamlrw.Value.equal v2 v1 in
257257+ if eq12 <> eq21 then fail "equal not symmetric" else check true
258258+259259+(** Test filter on lists *)
260260+let () =
261261+ add_test ~name:"value: filter" [ value ] @@ fun v ->
262262+ match v with
263263+ | `A _ ->
264264+ let filtered = Yamlrw.Value.filter (fun _ -> true) v in
265265+ if not (Yamlrw.Value.equal v filtered) then
266266+ fail "filter (fun _ -> true) should be identity"
267267+ else
268268+ let empty = Yamlrw.Value.filter (fun _ -> false) v in
269269+ (match empty with
270270+ | `A [] -> check true
271271+ | `A _ -> fail "filter (fun _ -> false) should be empty"
272272+ | _ -> fail "filter should preserve list type")
273273+ | _ -> check true
274274+275275+(** Test keys/values for objects *)
276276+let () =
277277+ add_test ~name:"value: keys/values" [ value ] @@ fun v ->
278278+ match v with
279279+ | `O pairs ->
280280+ let ks = Yamlrw.Value.keys v in
281281+ let vs = Yamlrw.Value.values v in
282282+ if List.length ks <> List.length pairs then fail "keys length mismatch"
283283+ else if List.length vs <> List.length pairs then
284284+ fail "values length mismatch"
285285+ else check true
286286+ | _ -> check true
287287+288288+(** Test Util.update *)
289289+let () =
290290+ add_test ~name:"value: Util.update" [ value; ident_string; value ]
291291+ @@ fun v key newv ->
292292+ match v with
293293+ | `O _ ->
294294+ (try
295295+ let updated = Yamlrw.Util.update key newv v in
296296+ let found = Yamlrw.Value.find key updated in
297297+ match found with
298298+ | Some x when Yamlrw.Value.equal x newv -> check true
299299+ | Some _ -> fail "update: found wrong value"
300300+ | None -> fail "update: key not found after update"
301301+ with Yamlrw.Util.Type_error _ -> fail "Type_error on update")
302302+ | _ -> check true
303303+304304+(** Test Util.remove *)
305305+let () =
306306+ add_test ~name:"value: Util.remove" [ value; ident_string ] @@ fun v key ->
307307+ match v with
308308+ | `O _ ->
309309+ (try
310310+ let removed = Yamlrw.Util.remove key v in
311311+ let found = Yamlrw.Value.find key removed in
312312+ if Option.is_some found then fail "remove: key still present"
313313+ else check true
314314+ with Yamlrw.Util.Type_error _ -> fail "Type_error on remove")
315315+ | _ -> check true
316316+317317+(** Test Util.get_path *)
318318+let () =
319319+ add_test ~name:"value: Util.get_path" [ value; list ident_string ]
320320+ @@ fun v path ->
321321+ let _ = Yamlrw.Util.get_path path v in
322322+ check true
323323+324324+(** Test Util.flatten *)
325325+let () =
326326+ add_test ~name:"value: Util.flatten" [ value ] @@ fun v ->
327327+ match v with
328328+ | `A _ ->
329329+ (try
330330+ let _ = Yamlrw.Util.flatten v in
331331+ check true
332332+ with Yamlrw.Util.Type_error _ -> fail "Type_error on flatten of list")
333333+ | _ -> check true
334334+335335+(** Test Util.nth *)
336336+let () =
337337+ add_test ~name:"value: Util.nth" [ value; range 100 ] @@ fun v idx ->
338338+ match v with
339339+ | `A lst ->
340340+ let result = Yamlrw.Util.nth idx v in
341341+ if idx < List.length lst then
342342+ match result with
343343+ | Some x when Yamlrw.Value.equal x (List.nth lst idx) -> check true
344344+ | Some _ -> fail "nth returned wrong element"
345345+ | None -> fail "nth returned None for valid index"
346346+ else if Option.is_some result then
347347+ fail "nth returned Some for invalid index"
348348+ else check true
349349+ | _ -> check true
350350+351351+(** Test Util.length *)
352352+let () =
353353+ add_test ~name:"value: Util.length" [ value ] @@ fun v ->
354354+ let len = Yamlrw.Util.length v in
355355+ (match v with
356356+ | `A lst when len = List.length lst -> ()
357357+ | `O pairs when len = List.length pairs -> ()
358358+ | `A _ -> fail "length mismatch for list"
359359+ | `O _ -> fail "length mismatch for object"
360360+ | _ when len = 0 -> ()
361361+ | _ -> fail "length should be 0 for scalars");
362362+ check true
363363+364364+(** Test Util.fold *)
365365+let () =
366366+ add_test ~name:"value: Util.fold" [ value ] @@ fun v ->
367367+ match v with
368368+ | `A lst ->
369369+ (try
370370+ let count = Yamlrw.Util.fold (fun acc _ -> acc + 1) 0 v in
371371+ if count <> List.length lst then fail "fold count mismatch"
372372+ else check true
373373+ with Yamlrw.Util.Type_error _ -> fail "Type_error on fold of list")
374374+ | _ -> check true
375375+376376+(** Test Util.mapi preserves length *)
377377+let () =
378378+ add_test ~name:"value: Util.mapi preserves length" [ value ] @@ fun v ->
379379+ match v with
380380+ | `A lst ->
381381+ (try
382382+ let mapped = Yamlrw.Util.mapi (fun _ x -> x) v in
383383+ (match mapped with
384384+ | `A lst' when List.length lst = List.length lst' -> check true
385385+ | `A _ -> fail "mapi changed list length"
386386+ | _ -> fail "mapi changed type")
387387+ with Yamlrw.Util.Type_error _ -> fail "Type_error on mapi of list")
388388+ | _ -> check true
389389+390390+let run () = ()
+458
ocaml-yamlrw/fuzz/fuzz_yamlrw.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Fuzz tests for the main Yamlrw parsing and serialization *)
77+88+open Crowbar
99+open Fuzz_common
1010+1111+(** Test that of_string never crashes on arbitrary input *)
1212+let () =
1313+ add_test ~name:"yamlrw: of_string crash safety" [ bytes ] @@ fun buf ->
1414+ (try
1515+ let _ = Yamlrw.of_string buf in
1616+ ()
1717+ with Yamlrw.Yamlrw_error _ -> ());
1818+ check true
1919+2020+(** Test that yaml_of_string never crashes on arbitrary input *)
2121+let () =
2222+ add_test ~name:"yamlrw: yaml_of_string crash safety" [ bytes ] @@ fun buf ->
2323+ (try
2424+ let _ = Yamlrw.yaml_of_string buf in
2525+ ()
2626+ with Yamlrw.Yamlrw_error _ -> ());
2727+ check true
2828+2929+(** Test that documents_of_string never crashes on arbitrary input *)
3030+let () =
3131+ add_test ~name:"yamlrw: documents_of_string crash safety" [ bytes ]
3232+ @@ fun buf ->
3333+ (try
3434+ let _ = Yamlrw.documents_of_string buf in
3535+ ()
3636+ with Yamlrw.Yamlrw_error _ -> ());
3737+ check true
3838+3939+(** Test roundtrip: parse -> serialize -> parse should give equal values *)
4040+let () =
4141+ add_test ~name:"yamlrw: value roundtrip" [ bytes ] @@ fun buf ->
4242+ match
4343+ try Some (Yamlrw.of_string buf) with Yamlrw.Yamlrw_error _ -> None
4444+ with
4545+ | None -> check true (* Invalid input is fine *)
4646+ | Some v1 ->
4747+ let serialized = Yamlrw.to_string v1 in
4848+ (match
4949+ try Some (Yamlrw.of_string serialized)
5050+ with Yamlrw.Yamlrw_error _ -> None
5151+ with
5252+ | None -> fail "re-parse of serialized output failed"
5353+ | Some v2 ->
5454+ if not (Yamlrw.equal v1 v2) then fail "roundtrip mismatch"
5555+ else check true)
5656+5757+(** Test yaml roundtrip - serializing and re-parsing should not crash.
5858+ Note: We don't check for value equality because YAML has ambiguous
5959+ edge cases (e.g., strings ending in ':' can be re-parsed as mapping keys). *)
6060+let () =
6161+ add_test ~name:"yamlrw: yaml roundtrip" [ bytes ] @@ fun buf ->
6262+ match
6363+ try Some (Yamlrw.yaml_of_string ~resolve_aliases:true buf)
6464+ with Yamlrw.Yamlrw_error _ -> None
6565+ with
6666+ | None -> check true
6767+ | Some y1 ->
6868+ let serialized = Yamlrw.yaml_to_string y1 in
6969+ (match
7070+ try Some (Yamlrw.yaml_of_string ~resolve_aliases:true serialized)
7171+ with Yamlrw.Yamlrw_error _ -> None
7272+ with
7373+ | None -> fail "re-parse of serialized yaml failed"
7474+ | Some _y2 ->
7575+ (* Just verify it parses - don't check equality due to YAML ambiguities *)
7676+ check true)
7777+7878+(** Test to_string never crashes for valid parsed values *)
7979+let () =
8080+ add_test ~name:"yamlrw: to_string after of_string" [ bytes ] @@ fun buf ->
8181+ (try
8282+ let v = Yamlrw.of_string buf in
8383+ let _ = Yamlrw.to_string v in
8484+ ()
8585+ with Yamlrw.Yamlrw_error _ -> ());
8686+ check true
8787+8888+(** Test pp never crashes *)
8989+let () =
9090+ add_test ~name:"yamlrw: pp" [ bytes ] @@ fun buf ->
9191+ (try
9292+ let v = Yamlrw.of_string buf in
9393+ let _ = Format.asprintf "%a" Yamlrw.pp v in
9494+ ()
9595+ with Yamlrw.Yamlrw_error _ -> ());
9696+ check true
9797+9898+(** Test equal is reflexive for parsed values *)
9999+let () =
100100+ add_test ~name:"yamlrw: equal reflexive" [ bytes ] @@ fun buf ->
101101+ (try
102102+ let v = Yamlrw.of_string buf in
103103+ if not (Yamlrw.equal v v) then fail "value not equal to itself" else ()
104104+ with Yamlrw.Yamlrw_error _ -> ());
105105+ check true
106106+107107+(** Test of_json/to_json roundtrip *)
108108+let () =
109109+ add_test ~name:"yamlrw: of_json/to_json roundtrip" [ bytes ] @@ fun buf ->
110110+ (try
111111+ let v = Yamlrw.of_string buf in
112112+ let y = Yamlrw.of_json v in
113113+ let v' = Yamlrw.to_json y in
114114+ if not (Yamlrw.equal v v') then fail "of_json/to_json roundtrip mismatch"
115115+ else ()
116116+ with Yamlrw.Yamlrw_error _ -> ());
117117+ check true
118118+119119+(** Test serialization with different styles *)
120120+let () =
121121+ add_test ~name:"yamlrw: to_string with block style" [ bytes ] @@ fun buf ->
122122+ (try
123123+ let v = Yamlrw.of_string buf in
124124+ let _ = Yamlrw.to_string ~layout_style:`Block v in
125125+ ()
126126+ with Yamlrw.Yamlrw_error _ -> ());
127127+ check true
128128+129129+let () =
130130+ add_test ~name:"yamlrw: to_string with flow style" [ bytes ] @@ fun buf ->
131131+ (try
132132+ let v = Yamlrw.of_string buf in
133133+ let _ = Yamlrw.to_string ~layout_style:`Flow v in
134134+ ()
135135+ with Yamlrw.Yamlrw_error _ -> ());
136136+ check true
137137+138138+(** Test simple valid YAML strings parse correctly *)
139139+let () =
140140+ add_test ~name:"yamlrw: simple string" [ printable_string ] @@ fun s ->
141141+ (* Wrap in quotes to ensure it's a valid YAML string *)
142142+ let yaml = "\"" ^ String.escaped s ^ "\"" in
143143+ (try
144144+ let _ = Yamlrw.of_string yaml in
145145+ ()
146146+ with Yamlrw.Yamlrw_error _ -> ());
147147+ check true
148148+149149+(** Test simple key-value mapping *)
150150+let () =
151151+ add_test ~name:"yamlrw: key-value mapping" [ ident_string; ident_string ]
152152+ @@ fun key value ->
153153+ if String.length key > 0 && String.length value > 0 then begin
154154+ let yaml = key ^ ": " ^ value in
155155+ match
156156+ try Some (Yamlrw.of_string yaml) with Yamlrw.Yamlrw_error _ -> None
157157+ with
158158+ | None -> check true
159159+ | Some v ->
160160+ (match v with
161161+ | `O [ (k, `String _) ] when k = key -> check true
162162+ | `O [ (k, `Float _) ] when k = key -> check true
163163+ | `O [ (k, `Bool _) ] when k = key -> check true
164164+ | `O [ (k, `Null) ] when k = key -> check true
165165+ | _ -> check true)
166166+ end
167167+ else check true
168168+169169+(** Test sequence parsing *)
170170+let () =
171171+ add_test ~name:"yamlrw: sequence" [ list ident_string ] @@ fun items ->
172172+ if List.length items > 0 && List.for_all (fun s -> String.length s > 0) items
173173+ then begin
174174+ let yaml = String.concat "\n" (List.map (fun s -> "- " ^ s) items) in
175175+ (try
176176+ let v = Yamlrw.of_string yaml in
177177+ match v with
178178+ | `A lst when List.length lst = List.length items -> ()
179179+ | `A _ -> fail "sequence length mismatch"
180180+ | _ -> fail "expected sequence"
181181+ with Yamlrw.Yamlrw_error _ -> ());
182182+ check true
183183+ end
184184+ else check true
185185+186186+(** Test document boundaries *)
187187+let () =
188188+ add_test ~name:"yamlrw: document markers" [ const () ] @@ fun () ->
189189+ let yaml = "---\nfoo: bar\n...\n---\nbaz: qux\n..." in
190190+ (try
191191+ let docs = Yamlrw.documents_of_string yaml in
192192+ if List.length docs <> 2 then fail "expected 2 documents" else ()
193193+ with Yamlrw.Yamlrw_error _ -> ());
194194+ check true
195195+196196+(** Test alias limits are enforced *)
197197+let () =
198198+ add_test ~name:"yamlrw: alias depth limit" [ const () ] @@ fun () ->
199199+ (* Create deeply nested alias structure *)
200200+ let yaml = "&a [*a]" in
201201+ (try
202202+ let _ = Yamlrw.of_string ~max_depth:5 yaml in
203203+ ()
204204+ with Yamlrw.Yamlrw_error _ -> ());
205205+ check true
206206+207207+(** Test buffer-based parsing *)
208208+let () =
209209+ add_test ~name:"yamlrw: of_buffer crash safety" [ bytes ] @@ fun buf ->
210210+ let buffer = Buffer.create (String.length buf) in
211211+ Buffer.add_string buffer buf;
212212+ (try
213213+ let _ = Yamlrw.of_buffer buffer in
214214+ ()
215215+ with Yamlrw.Yamlrw_error _ -> ());
216216+ check true
217217+218218+(** Test to_buffer produces parseable output *)
219219+let () =
220220+ add_test ~name:"yamlrw: to_buffer roundtrip" [ bytes ] @@ fun buf ->
221221+ (try
222222+ let v = Yamlrw.of_string buf in
223223+ let buffer = Yamlrw.to_buffer v in
224224+ let v' = Yamlrw.of_buffer buffer in
225225+ if not (Yamlrw.equal v v') then fail "to_buffer roundtrip mismatch" else ()
226226+ with Yamlrw.Yamlrw_error _ -> ());
227227+ check true
228228+229229+(** Test double roundtrip stabilizes - serialize twice should be identical *)
230230+let () =
231231+ add_test ~name:"yamlrw: double roundtrip stabilizes" [ bytes ] @@ fun buf ->
232232+ (try
233233+ let v1 = Yamlrw.of_string buf in
234234+ let s1 = Yamlrw.to_string v1 in
235235+ let v2 = Yamlrw.of_string s1 in
236236+ let s2 = Yamlrw.to_string v2 in
237237+ let v3 = Yamlrw.of_string s2 in
238238+ let s3 = Yamlrw.to_string v3 in
239239+ (* After two roundtrips, serialization should stabilize *)
240240+ if s2 <> s3 then fail "serialization did not stabilize after 2 roundtrips"
241241+ else if not (Yamlrw.equal v2 v3) then fail "values differ after stabilization"
242242+ else ()
243243+ with Yamlrw.Yamlrw_error _ -> ());
244244+ check true
245245+246246+(** Test cross-style roundtrip: parse any, emit block, re-parse *)
247247+let () =
248248+ add_test ~name:"yamlrw: cross-style block roundtrip" [ bytes ] @@ fun buf ->
249249+ (try
250250+ let v1 = Yamlrw.of_string buf in
251251+ let s_block = Yamlrw.to_string ~layout_style:`Block v1 in
252252+ let v2 = Yamlrw.of_string s_block in
253253+ if not (Yamlrw.equal v1 v2) then fail "block style roundtrip mismatch"
254254+ else ()
255255+ with Yamlrw.Yamlrw_error _ -> ());
256256+ check true
257257+258258+(** Test cross-style roundtrip: parse any, emit flow, re-parse *)
259259+let () =
260260+ add_test ~name:"yamlrw: cross-style flow roundtrip" [ bytes ] @@ fun buf ->
261261+ (try
262262+ let v1 = Yamlrw.of_string buf in
263263+ let s_flow = Yamlrw.to_string ~layout_style:`Flow v1 in
264264+ let v2 = Yamlrw.of_string s_flow in
265265+ if not (Yamlrw.equal v1 v2) then fail "flow style roundtrip mismatch"
266266+ else ()
267267+ with Yamlrw.Yamlrw_error _ -> ());
268268+ check true
269269+270270+(** Test scanner never crashes on arbitrary input *)
271271+let () =
272272+ add_test ~name:"yamlrw: scanner crash safety" [ bytes ] @@ fun buf ->
273273+ (try
274274+ let scanner = Yamlrw.Scanner.of_string buf in
275275+ let _ = Yamlrw.Scanner.to_list scanner in
276276+ ()
277277+ with Yamlrw.Yamlrw_error _ -> ());
278278+ check true
279279+280280+(** Test streaming parser never crashes *)
281281+let () =
282282+ add_test ~name:"yamlrw: stream parser crash safety" [ bytes ] @@ fun buf ->
283283+ (try
284284+ let parser = Yamlrw.Stream.parser buf in
285285+ Yamlrw.Stream.iter (fun _ _ _ -> ()) parser
286286+ with Yamlrw.Yamlrw_error _ -> ());
287287+ check true
288288+289289+(** Test that scanner tokens and parser events are consistent *)
290290+let () =
291291+ add_test ~name:"yamlrw: scanner/parser consistency" [ bytes ] @@ fun buf ->
292292+ let scanner_ok =
293293+ try
294294+ let scanner = Yamlrw.Scanner.of_string buf in
295295+ let _ = Yamlrw.Scanner.to_list scanner in
296296+ true
297297+ with Yamlrw.Yamlrw_error _ -> false
298298+ in
299299+ let parser_ok =
300300+ try
301301+ let parser = Yamlrw.Stream.parser buf in
302302+ Yamlrw.Stream.iter (fun _ _ _ -> ()) parser;
303303+ true
304304+ with Yamlrw.Yamlrw_error _ -> false
305305+ in
306306+ (* If scanner succeeds, parser should not crash (may still error on invalid structure) *)
307307+ if scanner_ok && not parser_ok then
308308+ (* This is actually OK - scanner can tokenize invalid YAML structure *)
309309+ check true
310310+ else check true
311311+312312+(** Test literal block scalar style *)
313313+let () =
314314+ add_test ~name:"yamlrw: literal block scalar" [ printable_string ] @@ fun s ->
315315+ if String.length s > 0 then begin
316316+ let yaml = "|\n " ^ String.concat "\n " (String.split_on_char '\n' s) in
317317+ (try
318318+ let _ = Yamlrw.of_string yaml in
319319+ ()
320320+ with Yamlrw.Yamlrw_error _ -> ());
321321+ check true
322322+ end
323323+ else check true
324324+325325+(** Test folded block scalar style *)
326326+let () =
327327+ add_test ~name:"yamlrw: folded block scalar" [ printable_string ] @@ fun s ->
328328+ if String.length s > 0 then begin
329329+ let yaml = ">\n " ^ String.concat "\n " (String.split_on_char '\n' s) in
330330+ (try
331331+ let _ = Yamlrw.of_string yaml in
332332+ ()
333333+ with Yamlrw.Yamlrw_error _ -> ());
334334+ check true
335335+ end
336336+ else check true
337337+338338+(** Test single-quoted scalar *)
339339+let () =
340340+ add_test ~name:"yamlrw: single quoted scalar" [ printable_string ] @@ fun s ->
341341+ (* Escape single quotes by doubling them *)
342342+ let escaped = Str.global_replace (Str.regexp "'") "''" s in
343343+ let yaml = "'" ^ escaped ^ "'" in
344344+ (try
345345+ let _ = Yamlrw.of_string yaml in
346346+ ()
347347+ with Yamlrw.Yamlrw_error _ -> ());
348348+ check true
349349+350350+(** Test double-quoted scalar with escape sequences *)
351351+let () =
352352+ add_test ~name:"yamlrw: double quoted with escapes" [ printable_string ]
353353+ @@ fun s ->
354354+ let yaml = "\"" ^ String.escaped s ^ "\"" in
355355+ (try
356356+ let _ = Yamlrw.of_string yaml in
357357+ ()
358358+ with Yamlrw.Yamlrw_error _ -> ());
359359+ check true
360360+361361+(** Test deeply nested structures don't crash *)
362362+let () =
363363+ add_test ~name:"yamlrw: deep nesting" [ range 50 ] @@ fun depth ->
364364+ let yaml = String.make depth '[' ^ "null" ^ String.make depth ']' in
365365+ (try
366366+ let _ = Yamlrw.of_string yaml in
367367+ ()
368368+ with Yamlrw.Yamlrw_error _ -> ());
369369+ check true
370370+371371+(** Test multiple anchors and aliases *)
372372+let () =
373373+ add_test ~name:"yamlrw: multiple anchors" [ ident_string; ident_string ]
374374+ @@ fun name1 name2 ->
375375+ if String.length name1 > 0 && String.length name2 > 0 then begin
376376+ let yaml =
377377+ Printf.sprintf "a: &%s value1\nb: &%s value2\nc: *%s\nd: *%s" name1 name2
378378+ name1 name2
379379+ in
380380+ (try
381381+ let _ = Yamlrw.of_string yaml in
382382+ ()
383383+ with Yamlrw.Yamlrw_error _ -> ());
384384+ check true
385385+ end
386386+ else check true
387387+388388+(** Test error positions are within input bounds *)
389389+let () =
390390+ add_test ~name:"yamlrw: error position bounds" [ bytes ] @@ fun buf ->
391391+ (try
392392+ let _ = Yamlrw.of_string buf in
393393+ ()
394394+ with Yamlrw.Yamlrw_error err ->
395395+ (* Error has span : Span.t option, and Span has start/end positions *)
396396+ match err.span with
397397+ | None -> () (* No position info, that's ok *)
398398+ | Some span ->
399399+ let start_pos = span.start in
400400+ let line = start_pos.Yamlrw.Position.line in
401401+ let col = start_pos.Yamlrw.Position.column in
402402+ let offset = start_pos.Yamlrw.Position.index in
403403+ if line < 1 then fail "error line < 1"
404404+ else if col < 0 then fail "error column < 0"
405405+ else if offset < 0 then fail "error offset < 0"
406406+ else if offset > String.length buf then
407407+ fail "error offset > input length"
408408+ else ());
409409+ check true
410410+411411+(** Test yaml_of_string with resolve_aliases=true vs false *)
412412+let () =
413413+ add_test ~name:"yamlrw: yaml resolve_aliases modes" [ bytes ] @@ fun buf ->
414414+ let with_resolve =
415415+ try Some (Yamlrw.yaml_of_string ~resolve_aliases:true buf)
416416+ with Yamlrw.Yamlrw_error _ -> None
417417+ in
418418+ let without_resolve =
419419+ try Some (Yamlrw.yaml_of_string ~resolve_aliases:false buf)
420420+ with Yamlrw.Yamlrw_error _ -> None
421421+ in
422422+ (* Both should either succeed or fail, but not crash *)
423423+ (match (with_resolve, without_resolve) with
424424+ | Some y1, Some _y2 ->
425425+ (* If both succeed, serializing resolved version should work *)
426426+ let _ = Yamlrw.yaml_to_string y1 in
427427+ ()
428428+ | _ -> ());
429429+ check true
430430+431431+(** Test documents roundtrip with resolve_aliases=false preserves structure *)
432432+let () =
433433+ add_test ~name:"yamlrw: documents roundtrip (no resolve)" [ bytes ] @@ fun buf ->
434434+ (try
435435+ let docs = Yamlrw.documents_of_string buf in
436436+ let serialized = Yamlrw.documents_to_string ~resolve_aliases:false docs in
437437+ let docs' = Yamlrw.documents_of_string serialized in
438438+ if List.length docs <> List.length docs' then
439439+ fail "document count mismatch after roundtrip (no resolve)"
440440+ else ()
441441+ with Yamlrw.Yamlrw_error _ -> ());
442442+ check true
443443+444444+(** Test documents roundtrip with resolve_aliases=true *)
445445+let () =
446446+ add_test ~name:"yamlrw: documents roundtrip (resolve)" [ bytes ] @@ fun buf ->
447447+ (try
448448+ let docs = Yamlrw.documents_of_string buf in
449449+ let serialized = Yamlrw.documents_to_string ~resolve_aliases:true docs in
450450+ (* With resolve_aliases=true, anchors are stripped. Empty scalars with
451451+ only anchors become truly empty, which may reduce document count.
452452+ We just verify re-parsing doesn't crash. *)
453453+ let _ = Yamlrw.documents_of_string serialized in
454454+ ()
455455+ with Yamlrw.Yamlrw_error _ -> ());
456456+ check true
457457+458458+let run () = ()
···169169 Buffer.contents buf
170170 end
171171172172-(** Write scalar with appropriate quoting *)
172172+(** Write indentation for block scalar content.
173173+ Block scalar content must be indented by at least 1 space more than the
174174+ containing structure. We use config.indent spaces, ensuring at least 1. *)
175175+let write_block_scalar_indent t =
176176+ let content_indent = max 1 t.config.indent in
177177+ for _ = 1 to t.indent + content_indent do
178178+ write_char t ' '
179179+ done
180180+181181+(** Write scalar with appropriate quoting.
182182+ Returns true if the scalar ends with a newline (block scalars), false otherwise.
183183+ Callers should check this to avoid double newlines. *)
173184let write_scalar t ?(style = `Any) value =
174185 match match style with `Any -> Quoting.choose_style value | s -> s with
175175- | `Plain | `Any -> write t value
186186+ | `Plain | `Any ->
187187+ write t value;
188188+ false
176189 | `Single_quoted ->
177190 write_char t '\'';
178191 write t (escape_single_quoted value);
179179- write_char t '\''
192192+ write_char t '\'';
193193+ false
180194 | `Double_quoted ->
181195 write_char t '"';
182196 write t (escape_double_quoted value);
183183- write_char t '"'
197197+ write_char t '"';
198198+ false
184199 | `Literal ->
185200 write t "|";
186201 write_newline t;
187187- String.split_on_char '\n' value
188188- |> List.iter (fun line ->
189189- write_indent t;
190190- write t line;
191191- write_newline t)
202202+ let lines = String.split_on_char '\n' value in
203203+ let rec write_lines = function
204204+ | [] -> ()
205205+ | [ last ] ->
206206+ write_block_scalar_indent t;
207207+ write t last
208208+ (* No trailing newline - caller will add it *)
209209+ | line :: rest ->
210210+ write_block_scalar_indent t;
211211+ write t line;
212212+ write_newline t;
213213+ write_lines rest
214214+ in
215215+ write_lines lines;
216216+ true (* Block scalar ends with content on last line, needs newline from caller *)
192217 | `Folded ->
193218 write t ">";
194219 write_newline t;
195195- String.split_on_char '\n' value
196196- |> List.iter (fun line ->
197197- write_indent t;
198198- write t line;
199199- write_newline t)
220220+ let lines = String.split_on_char '\n' value in
221221+ let rec write_lines = function
222222+ | [] -> ()
223223+ | [ last ] ->
224224+ write_block_scalar_indent t;
225225+ write t last
226226+ (* No trailing newline - caller will add it *)
227227+ | line :: rest ->
228228+ write_block_scalar_indent t;
229229+ write t line;
230230+ write_newline t;
231231+ write_lines rest
232232+ in
233233+ write_lines lines;
234234+ true (* Block scalar ends with content on last line, needs newline from caller *)
200235201236(** Write anchor if present *)
202237let write_anchor t anchor =
···275310 if t.need_separator then write t ", ";
276311 write_anchor t anchor;
277312 write_tag t ~implicit:plain_implicit tag;
278278- write_scalar t ~style value;
313313+ let (_ : bool) = write_scalar t ~style value in
279314 write t ": ";
280315 t.need_separator <- false;
281316 t.state <- In_flow_mapping_value
···286321 write t ", ";
287322 write_anchor t anchor;
288323 write_tag t ~implicit:plain_implicit tag;
289289- write_scalar t ~style value;
324324+ let (_ : bool) = write_scalar t ~style value in
290325 write t ": ";
291326 t.need_separator <- false;
292327 t.state <- In_flow_mapping_value
···295330 (* Normal value scalar *)
296331 write_anchor t anchor;
297332 write_tag t ~implicit:plain_implicit tag;
298298- write_scalar t ~style value;
333333+ let (_ : bool) = write_scalar t ~style value in
299334 t.need_separator <- true;
300335 t.state <- In_flow_mapping_key
301336 end
···304339 t.need_separator <- true;
305340 write_anchor t anchor;
306341 write_tag t ~implicit:plain_implicit tag;
307307- write_scalar t ~style value
342342+ let (_ : bool) = write_scalar t ~style value in
343343+ ()
308344 end
309345 else begin
310346 match t.state with
···313349 write t "- ";
314350 write_anchor t anchor;
315351 write_tag t ~implicit:plain_implicit tag;
316316- write_scalar t ~style value;
352352+ let (_ : bool) = write_scalar t ~style value in
317353 write_newline t
318354 | In_block_mapping_key indent ->
319355 write_indent t;
320356 write_anchor t anchor;
321357 write_tag t ~implicit:plain_implicit tag;
322322- write_scalar t ~style value;
358358+ let (_ : bool) = write_scalar t ~style value in
323359 write_char t ':';
324360 t.state <- In_block_mapping_value indent
325361 | In_block_mapping_first_key indent ->
326362 (* First key after "- ", no indent needed *)
327363 write_anchor t anchor;
328364 write_tag t ~implicit:plain_implicit tag;
329329- write_scalar t ~style value;
365365+ let (_ : bool) = write_scalar t ~style value in
330366 write_char t ':';
331367 t.state <- In_block_mapping_value indent
332368 | In_block_mapping_value indent ->
333369 write_char t ' ';
334370 write_anchor t anchor;
335371 write_tag t ~implicit:plain_implicit tag;
336336- write_scalar t ~style value;
372372+ let (_ : bool) = write_scalar t ~style value in
337373 write_newline t;
338374 t.state <- In_block_mapping_key indent
339375 | _ ->
340376 write_anchor t anchor;
341377 write_tag t ~implicit:plain_implicit tag;
342342- write_scalar t ~style value;
378378+ let (_ : bool) = write_scalar t ~style value in
343379 write_newline t
344380 end
345381 | Event.Sequence_start { anchor; tag; implicit; style } ->
+3
ocaml-yamlrw/lib/scanner.ml
···534534 | _ -> Error.raise_at start (Invalid_hex_escape (Buffer.contents buf))
535535 done;
536536 let code = int_of_string ("0x" ^ Buffer.contents buf) in
537537+ (* Validate Unicode scalar value (0x0000-0x10FFFF, excluding surrogates) *)
538538+ if not (Uchar.is_valid code) then
539539+ Error.raise_at start (Invalid_unicode_escape (Buffer.contents buf));
537540 if code <= 0x7F then String.make 1 (Char.chr code)
538541 else if code <= 0x7FF then
539542 let b1 = 0xC0 lor (code lsr 6) in
+17-23
ocaml-yamlrw/lib/yaml.ml
···120120 v
121121 | `Alias name -> expand_alias ~depth name
122122 | `A seq ->
123123- (* First resolve all members in order *)
123123+ (* Register anchor with ORIGINAL node BEFORE resolving members.
124124+ This ensures that when this anchor is expanded later through
125125+ an alias chain, the internal aliases still need resolution,
126126+ allowing the depth counter to properly accumulate. *)
127127+ Option.iter (fun name -> register_anchor name v) (Sequence.anchor seq);
128128+ (* Now resolve all members in order *)
124129 let resolved_members =
125130 List.map (resolve ~depth) (Sequence.members seq)
126131 in
127127- let resolved =
128128- `A
129129- (Sequence.make ?anchor:(Sequence.anchor seq) ?tag:(Sequence.tag seq)
130130- ~implicit:(Sequence.implicit seq) ~style:(Sequence.style seq)
131131- resolved_members)
132132- in
133133- (* Register anchor with resolved node *)
134134- Option.iter
135135- (fun name -> register_anchor name resolved)
136136- (Sequence.anchor seq);
137137- resolved
132132+ `A
133133+ (Sequence.make ?anchor:(Sequence.anchor seq) ?tag:(Sequence.tag seq)
134134+ ~implicit:(Sequence.implicit seq) ~style:(Sequence.style seq)
135135+ resolved_members)
138136 | `O map ->
137137+ (* Register anchor with ORIGINAL node BEFORE resolving members.
138138+ This ensures proper depth tracking for alias chains. *)
139139+ Option.iter (fun name -> register_anchor name v) (Mapping.anchor map);
139140 (* Process key-value pairs in document order *)
140141 let resolved_pairs =
141142 List.map
···145146 (resolved_k, resolved_v))
146147 (Mapping.members map)
147148 in
148148- let resolved =
149149- `O
150150- (Mapping.make ?anchor:(Mapping.anchor map) ?tag:(Mapping.tag map)
151151- ~implicit:(Mapping.implicit map) ~style:(Mapping.style map)
152152- resolved_pairs)
153153- in
154154- (* Register anchor with resolved node *)
155155- Option.iter
156156- (fun name -> register_anchor name resolved)
157157- (Mapping.anchor map);
158158- resolved
149149+ `O
150150+ (Mapping.make ?anchor:(Mapping.anchor map) ?tag:(Mapping.tag map)
151151+ ~implicit:(Mapping.implicit map) ~style:(Mapping.style map)
152152+ resolved_pairs)
159153 in
160154 resolve ~depth:0 root
161155
+1-1
ocaml-yamlrw/tests/cram/bomb.t
···1414Test depth limit with a nested alias chain:
15151616 $ yamlcat --max-depth 2 --json depth_bomb.yml | head -c 50
1717- {"a": ["x", "y", "z"], "b": [["x", "y", "z"], ["x"
1717+ Error: alias expansion exceeded depth limit (2 levels)
18181919 $ yamlcat --max-depth 10 --json depth_bomb.yml | head -c 50
2020 {"a": ["x", "y", "z"], "b": [["x", "y", "z"], ["x"