My aggregated monorepo of OCaml code, automaintained
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge commit '0f9fd6e30e3bdfca0bc029b57efccf837812f3d1'

+1966 -51
+2 -1
ocaml-yamlrw/dune-project
··· 28 28 (odoc :with-doc) 29 29 (mdx :with-doc) 30 30 (jsonm :with-test) 31 - (alcotest :with-test))) 31 + (alcotest :with-test) 32 + (crowbar :with-test))) 32 33 33 34 (package 34 35 (name yamlrw-unix)
+10
ocaml-yamlrw/dune-workspace
··· 1 + (lang dune 3.21) 2 + 3 + ; AFL instrumentation profile for fuzz testing 4 + ; Usage: 5 + ; dune build --profile=afl @fuzz-afl 6 + ; afl-fuzz -m none -i fuzz/input -o _fuzz -- _build/default/fuzz/fuzz_afl.exe @@ 7 + ; 8 + (env 9 + (afl 10 + (ocamlopt_flags (:standard -afl-instrument))))
+67
ocaml-yamlrw/fuzz/dune
··· 1 + ; Fuzz testing with Crowbar 2 + ; 3 + ; Quick check (runs all tests with random inputs): 4 + ; dune build @fuzz 5 + ; -- or -- 6 + ; dune exec fuzz/fuzz.exe 7 + ; 8 + ; With AFL instrumentation for thorough fuzzing: 9 + ; dune build --profile=afl @fuzz-afl # build the fuzzer 10 + ; dune build --profile=afl @run-afl # run afl-fuzz (interactive) 11 + ; 12 + ; Note: AFL profile requires an OCaml compiler with AFL support: 13 + ; opam switch create ./afl ocaml-variants.5.2.0+options ocaml-option-afl 14 + 15 + (executable 16 + (name fuzz) 17 + (libraries crowbar yamlrw) 18 + (modules 19 + fuzz 20 + fuzz_common 21 + fuzz_encoding 22 + fuzz_chomping 23 + fuzz_tag 24 + fuzz_value 25 + fuzz_yamlrw 26 + fuzz_emitter)) 27 + 28 + ; Standalone AFL fuzzer for targeted parser testing 29 + ; This is a simpler executable that directly reads input and exercises the parser 30 + ; Best used with AFL instrumentation for finding parser bugs 31 + 32 + (executable 33 + (name fuzz_afl) 34 + (libraries yamlrw) 35 + (modules fuzz_afl)) 36 + 37 + ; Alias to run Crowbar fuzz tests (quick check mode) 38 + (rule 39 + (alias fuzz) 40 + (deps 41 + (source_tree input)) 42 + (action 43 + (run %{exe:fuzz.exe}))) 44 + 45 + ; Alias to build AFL-instrumented fuzzer 46 + ; Use with: dune build --profile=afl @fuzz-afl 47 + (rule 48 + (alias fuzz-afl) 49 + (deps 50 + (source_tree input) 51 + fuzz_afl.exe) 52 + (action 53 + (echo "AFL fuzzer built. To run: dune exec --profile=afl @run-afl\n"))) 54 + 55 + ; Alias to run AFL fuzzer 56 + ; Use with: dune build --profile=afl @run-afl 57 + ; Set AFL_TIMEOUT to control duration in seconds (default: 300 = 5 minutes) 58 + ; Example: AFL_TIMEOUT=3600 dune build --profile=afl @run-afl # 1 hour 59 + (rule 60 + (alias run-afl) 61 + (deps 62 + (source_tree input) 63 + fuzz_afl.exe) 64 + (action 65 + (setenv AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES 1 66 + (setenv AFL_SKIP_CPUFREQ 1 67 + (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
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + (** Main entry point for fuzz tests. 7 + 8 + Run without arguments for Crowbar's default mode (quick check): 9 + {[ 10 + dune exec fuzz/fuzz.exe 11 + ]} 12 + 13 + Run with AFL for thorough fuzzing: 14 + {[ 15 + mkdir -p fuzz/input 16 + echo -n "" > fuzz/input/empty 17 + echo "key: value" > fuzz/input/simple 18 + echo -e "- a\n- b\n- c" > fuzz/input/list 19 + afl-fuzz -m none -i fuzz/input -o _fuzz -- _build/default/fuzz/fuzz.exe @@ 20 + ]} 21 + 22 + For AFL mode, build with afl-instrument: 23 + {[ 24 + opam install crowbar afl-persistent 25 + dune build fuzz/fuzz.exe 26 + ]} *) 27 + 28 + (* Force linking of all fuzz test modules via side effects *) 29 + let () = 30 + Fuzz_common.run (); 31 + Fuzz_encoding.run (); 32 + Fuzz_chomping.run (); 33 + Fuzz_tag.run (); 34 + Fuzz_value.run (); 35 + Fuzz_yamlrw.run (); 36 + Fuzz_emitter.run ()
+114
ocaml-yamlrw/fuzz/fuzz_afl.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + (** AFL-specific fuzzer for yamlrw parser. 7 + 8 + This is a standalone AFL fuzzer that reads input from a file or stdin 9 + and exercises the parser. Build with afl-instrument for best results. 10 + 11 + Usage: 12 + {[ 13 + # Build with AFL instrumentation 14 + opam switch create . ocaml-variants.5.2.0+options ocaml-option-afl 15 + dune build fuzz/fuzz_afl.exe 16 + 17 + # Create seed corpus 18 + mkdir -p fuzz/input 19 + echo -n "" > fuzz/input/empty 20 + echo "null" > fuzz/input/null 21 + echo "true" > fuzz/input/bool 22 + echo "42" > fuzz/input/int 23 + echo "3.14" > fuzz/input/float 24 + echo "hello" > fuzz/input/string 25 + echo "key: value" > fuzz/input/mapping 26 + echo -e "- a\n- b" > fuzz/input/sequence 27 + echo -e "---\nfoo\n..." > fuzz/input/document 28 + echo "&anchor value" > fuzz/input/anchor 29 + echo "!tag value" > fuzz/input/tag 30 + echo -e "|\n literal\n block" > fuzz/input/literal 31 + echo -e ">\n folded\n block" > fuzz/input/folded 32 + echo "'single quoted'" > fuzz/input/single 33 + echo '"double quoted"' > fuzz/input/double 34 + 35 + # Run AFL 36 + afl-fuzz -m none -i fuzz/input -o _fuzz -- _build/default/fuzz/fuzz_afl.exe @@ 37 + ]} *) 38 + 39 + (** Read entire file as string *) 40 + let read_file filename = 41 + let ic = open_in_bin filename in 42 + let n = in_channel_length ic in 43 + let s = really_input_string ic n in 44 + close_in ic; 45 + s 46 + 47 + (** Read from stdin until EOF *) 48 + let read_stdin () = 49 + let buf = Buffer.create 1024 in 50 + try 51 + while true do 52 + Buffer.add_channel buf stdin 1024 53 + done; 54 + assert false 55 + with End_of_file -> Buffer.contents buf 56 + 57 + (** Fuzz target: exercises all major parsing paths *) 58 + let fuzz_target input = 59 + (* Test value parsing *) 60 + (try 61 + let v = Yamlrw.of_string input in 62 + (* Exercise serialization *) 63 + let _ = Yamlrw.to_string v in 64 + (* Exercise different styles *) 65 + let _ = Yamlrw.to_string ~layout_style:`Block v in 66 + let _ = Yamlrw.to_string ~layout_style:`Flow v in 67 + (* Exercise pp *) 68 + let _ = Format.asprintf "%a" Yamlrw.pp v in 69 + () 70 + with Yamlrw.Yamlrw_error _ -> ()); 71 + 72 + (* Test yaml parsing (with alias resolution) *) 73 + (try 74 + let y = Yamlrw.yaml_of_string ~resolve_aliases:true input in 75 + let _ = Yamlrw.yaml_to_string y in 76 + () 77 + with Yamlrw.Yamlrw_error _ -> ()); 78 + 79 + (* Test yaml parsing (without alias resolution) *) 80 + (try 81 + let y = Yamlrw.yaml_of_string ~resolve_aliases:false input in 82 + let _ = Yamlrw.yaml_to_string y in 83 + () 84 + with Yamlrw.Yamlrw_error _ -> ()); 85 + 86 + (* Test document parsing *) 87 + (try 88 + let docs = Yamlrw.documents_of_string input in 89 + let _ = Yamlrw.documents_to_string docs in 90 + () 91 + with Yamlrw.Yamlrw_error _ -> ()); 92 + 93 + (* Test encoding detection *) 94 + let enc, _ = Yamlrw.Encoding.detect input in 95 + let _ = Yamlrw.Encoding.to_string enc in 96 + 97 + (* Test streaming parser *) 98 + (try 99 + let parser = Yamlrw.Stream.parser input in 100 + Yamlrw.Stream.iter (fun _ _ _ -> ()) parser 101 + with Yamlrw.Yamlrw_error _ -> ()); 102 + 103 + (* Test scanner directly *) 104 + (try 105 + let scanner = Yamlrw.Scanner.of_string input in 106 + let _ = Yamlrw.Scanner.to_list scanner in 107 + () 108 + with Yamlrw.Yamlrw_error _ -> ()) 109 + 110 + let () = 111 + let input = 112 + if Array.length Sys.argv > 1 then read_file Sys.argv.(1) else read_stdin () 113 + in 114 + fuzz_target input
+93
ocaml-yamlrw/fuzz/fuzz_chomping.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + (** Fuzz tests for Chomping module *) 7 + 8 + open Crowbar 9 + 10 + (** Test of_char/to_char roundtrip for valid chars *) 11 + let () = 12 + add_test ~name:"chomping: of_char/to_char roundtrip" [ uint8 ] @@ fun n -> 13 + let c = Char.chr n in 14 + match Yamlrw.Chomping.of_char c with 15 + | None -> check true (* Invalid char, that's fine *) 16 + | Some chomping -> ( 17 + match Yamlrw.Chomping.to_char chomping with 18 + | None -> 19 + (* Clip has no char representation *) 20 + if chomping <> Yamlrw.Chomping.Clip then 21 + fail "non-Clip chomping should have char" 22 + else check true 23 + | Some c' -> 24 + if c <> c' then fail "roundtrip mismatch" 25 + else check true) 26 + 27 + (** Test that to_string never crashes *) 28 + let () = 29 + add_test ~name:"chomping: to_string Strip" [ const () ] @@ fun () -> 30 + let _ = Yamlrw.Chomping.to_string Yamlrw.Chomping.Strip in 31 + check true 32 + 33 + let () = 34 + add_test ~name:"chomping: to_string Clip" [ const () ] @@ fun () -> 35 + let _ = Yamlrw.Chomping.to_string Yamlrw.Chomping.Clip in 36 + check true 37 + 38 + let () = 39 + add_test ~name:"chomping: to_string Keep" [ const () ] @@ fun () -> 40 + let _ = Yamlrw.Chomping.to_string Yamlrw.Chomping.Keep in 41 + check true 42 + 43 + (** Test pp never crashes *) 44 + let () = 45 + add_test ~name:"chomping: pp" [ range 3 ] @@ fun n -> 46 + let chomping = 47 + match n with 48 + | 0 -> Yamlrw.Chomping.Strip 49 + | 1 -> Yamlrw.Chomping.Clip 50 + | _ -> Yamlrw.Chomping.Keep 51 + in 52 + let _ = Format.asprintf "%a" Yamlrw.Chomping.pp chomping in 53 + check true 54 + 55 + (** Test equality is reflexive *) 56 + let () = 57 + add_test ~name:"chomping: equal reflexive" [ range 3 ] @@ fun n -> 58 + let chomping = 59 + match n with 60 + | 0 -> Yamlrw.Chomping.Strip 61 + | 1 -> Yamlrw.Chomping.Clip 62 + | _ -> Yamlrw.Chomping.Keep 63 + in 64 + if not (Yamlrw.Chomping.equal chomping chomping) then 65 + fail "chomping not equal to itself" 66 + else check true 67 + 68 + (** Test specific valid indicators *) 69 + let () = 70 + add_test ~name:"chomping: strip indicator '-'" [ const () ] @@ fun () -> 71 + match Yamlrw.Chomping.of_char '-' with 72 + | Some Yamlrw.Chomping.Strip -> check true 73 + | _ -> fail "'-' should parse as Strip" 74 + 75 + let () = 76 + add_test ~name:"chomping: keep indicator '+'" [ const () ] @@ fun () -> 77 + match Yamlrw.Chomping.of_char '+' with 78 + | Some Yamlrw.Chomping.Keep -> check true 79 + | _ -> fail "'+' should parse as Keep" 80 + 81 + (** Test invalid chars return None *) 82 + let () = 83 + add_test ~name:"chomping: invalid chars" [ const () ] @@ fun () -> 84 + let invalid_chars = [ 'a'; 'z'; '0'; '9'; ' '; '\n'; '#' ] in 85 + List.iter 86 + (fun c -> 87 + match Yamlrw.Chomping.of_char c with 88 + | None -> () 89 + | Some _ -> fail (Printf.sprintf "char '%c' should not be valid" c)) 90 + invalid_chars; 91 + check true 92 + 93 + let run () = ()
+55
ocaml-yamlrw/fuzz/fuzz_common.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + (** Common utilities for fuzz tests. *) 7 + 8 + open Crowbar 9 + 10 + let to_bytes buf = 11 + let len = String.length buf in 12 + let b = Bytes.create len in 13 + Bytes.blit_string buf 0 b 0 len; 14 + b 15 + 16 + (** Generator for printable ASCII strings (useful for YAML content) *) 17 + let printable_char = map [ range 95 ] (fun n -> Char.chr (n + 32)) 18 + 19 + let printable_string = 20 + map [ list printable_char ] (fun chars -> 21 + String.init (List.length chars) (List.nth chars)) 22 + 23 + (** Generator for valid YAML scalar content (excludes problematic chars) *) 24 + let yaml_safe_char = 25 + map [ range 94 ] (fun n -> 26 + let c = n + 32 in 27 + (* Skip colon, hash, and other YAML special chars at start *) 28 + if c = 58 (* : *) || c = 35 (* # *) then Char.chr 97 (* 'a' *) 29 + else Char.chr c) 30 + 31 + let yaml_safe_string = 32 + map [ list yaml_safe_char ] (fun chars -> 33 + String.init (List.length chars) (List.nth chars)) 34 + 35 + (** Generator for identifier-like strings *) 36 + let ident_char = 37 + map [ range 62 ] (fun n -> 38 + if n < 26 then Char.chr (n + 97) (* a-z *) 39 + else if n < 52 then Char.chr (n - 26 + 65) (* A-Z *) 40 + else if n < 62 then Char.chr (n - 52 + 48) (* 0-9 *) 41 + else '_') 42 + 43 + let ident_string = 44 + map [ list1 ident_char ] (fun chars -> 45 + String.init (List.length chars) (List.nth chars)) 46 + 47 + (** Catch exceptions and pass the test if expected exception occurs *) 48 + let catch_invalid_arg f = 49 + try f () with Invalid_argument _ -> check true 50 + 51 + let catch_yamlrw_error f = 52 + try f () 53 + with Yamlrw.Yamlrw_error _ -> check true 54 + 55 + let run () = ()
+283
ocaml-yamlrw/fuzz/fuzz_emitter.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + (** Fuzz tests for the Emitter module - test random event sequences *) 7 + 8 + open Crowbar 9 + open Fuzz_common 10 + 11 + (** Event type for fuzzing *) 12 + type fuzz_event = 13 + | Stream_start 14 + | Stream_end 15 + | Doc_start 16 + | Doc_end 17 + | Scalar of string 18 + | Alias of string 19 + | Seq_start 20 + | Seq_end 21 + | Map_start 22 + | Map_end 23 + 24 + (** Generator for fuzz events *) 25 + let fuzz_event = 26 + choose 27 + [ 28 + const Stream_start; 29 + const Stream_end; 30 + const Doc_start; 31 + const Doc_end; 32 + map [ ident_string ] (fun s -> Scalar s); 33 + map [ ident_string ] (fun s -> Alias s); 34 + const Seq_start; 35 + const Seq_end; 36 + const Map_start; 37 + const Map_end; 38 + ] 39 + 40 + (** Emit a fuzz event to an emitter - may fail with Yamlrw_error *) 41 + let emit_fuzz_event emitter = function 42 + | Stream_start -> Yamlrw.Stream.stream_start emitter `Utf8 43 + | Stream_end -> Yamlrw.Stream.stream_end emitter 44 + | Doc_start -> Yamlrw.Stream.document_start emitter () 45 + | Doc_end -> Yamlrw.Stream.document_end emitter () 46 + | Scalar s -> Yamlrw.Stream.scalar emitter s 47 + | Alias s -> Yamlrw.Stream.alias emitter s 48 + | Seq_start -> Yamlrw.Stream.sequence_start emitter () 49 + | Seq_end -> Yamlrw.Stream.sequence_end emitter 50 + | Map_start -> Yamlrw.Stream.mapping_start emitter () 51 + | Map_end -> Yamlrw.Stream.mapping_end emitter 52 + 53 + (** Test that random event sequences don't crash the emitter *) 54 + let () = 55 + add_test ~name:"emitter: random events crash safety" [ list fuzz_event ] 56 + @@ fun events -> 57 + let emitter = Yamlrw.Stream.emitter () in 58 + List.iter 59 + (fun ev -> 60 + try emit_fuzz_event emitter ev with Yamlrw.Yamlrw_error _ -> ()) 61 + events; 62 + check true 63 + 64 + (** Test that valid event sequences produce parseable output *) 65 + let () = 66 + add_test ~name:"emitter: valid sequence roundtrip" [ list ident_string ] 67 + @@ fun items -> 68 + if List.length items > 0 then begin 69 + let emitter = Yamlrw.Stream.emitter () in 70 + (try 71 + Yamlrw.Stream.stream_start emitter `Utf8; 72 + Yamlrw.Stream.document_start emitter (); 73 + Yamlrw.Stream.sequence_start emitter (); 74 + List.iter (fun s -> Yamlrw.Stream.scalar emitter s) items; 75 + Yamlrw.Stream.sequence_end emitter; 76 + Yamlrw.Stream.document_end emitter (); 77 + Yamlrw.Stream.stream_end emitter; 78 + let yaml = Yamlrw.Stream.contents emitter in 79 + (* Try to parse the emitted YAML *) 80 + let _ = Yamlrw.of_string yaml in 81 + () 82 + with Yamlrw.Yamlrw_error _ -> ()); 83 + check true 84 + end 85 + else check true 86 + 87 + (** Test that valid mapping event sequences produce parseable output *) 88 + let () = 89 + add_test ~name:"emitter: valid mapping roundtrip" 90 + [ list (pair ident_string ident_string) ] 91 + @@ fun pairs -> 92 + if List.length pairs > 0 then begin 93 + let emitter = Yamlrw.Stream.emitter () in 94 + (try 95 + Yamlrw.Stream.stream_start emitter `Utf8; 96 + Yamlrw.Stream.document_start emitter (); 97 + Yamlrw.Stream.mapping_start emitter (); 98 + List.iter 99 + (fun (k, v) -> 100 + Yamlrw.Stream.scalar emitter k; 101 + Yamlrw.Stream.scalar emitter v) 102 + pairs; 103 + Yamlrw.Stream.mapping_end emitter; 104 + Yamlrw.Stream.document_end emitter (); 105 + Yamlrw.Stream.stream_end emitter; 106 + let yaml = Yamlrw.Stream.contents emitter in 107 + (* Try to parse the emitted YAML *) 108 + let _ = Yamlrw.of_string yaml in 109 + () 110 + with Yamlrw.Yamlrw_error _ -> ()); 111 + check true 112 + end 113 + else check true 114 + 115 + (** Test nested sequences *) 116 + let () = 117 + add_test ~name:"emitter: nested sequences" [ range 10; list ident_string ] 118 + @@ fun depth items -> 119 + if depth > 0 && List.length items > 0 then begin 120 + let emitter = Yamlrw.Stream.emitter () in 121 + (try 122 + Yamlrw.Stream.stream_start emitter `Utf8; 123 + Yamlrw.Stream.document_start emitter (); 124 + for _ = 1 to depth do 125 + Yamlrw.Stream.sequence_start emitter () 126 + done; 127 + List.iter (fun s -> Yamlrw.Stream.scalar emitter s) items; 128 + for _ = 1 to depth do 129 + Yamlrw.Stream.sequence_end emitter 130 + done; 131 + Yamlrw.Stream.document_end emitter (); 132 + Yamlrw.Stream.stream_end emitter; 133 + let yaml = Yamlrw.Stream.contents emitter in 134 + let _ = Yamlrw.of_string yaml in 135 + () 136 + with Yamlrw.Yamlrw_error _ -> ()); 137 + check true 138 + end 139 + else check true 140 + 141 + (** Test nested mappings *) 142 + let () = 143 + add_test ~name:"emitter: nested mappings" [ range 10; ident_string ] 144 + @@ fun depth value -> 145 + if depth > 0 && String.length value > 0 then begin 146 + let emitter = Yamlrw.Stream.emitter () in 147 + (try 148 + Yamlrw.Stream.stream_start emitter `Utf8; 149 + Yamlrw.Stream.document_start emitter (); 150 + for i = 1 to depth do 151 + Yamlrw.Stream.mapping_start emitter (); 152 + Yamlrw.Stream.scalar emitter (Printf.sprintf "key%d" i) 153 + done; 154 + Yamlrw.Stream.scalar emitter value; 155 + for _ = 1 to depth do 156 + Yamlrw.Stream.mapping_end emitter 157 + done; 158 + Yamlrw.Stream.document_end emitter (); 159 + Yamlrw.Stream.stream_end emitter; 160 + let yaml = Yamlrw.Stream.contents emitter in 161 + let _ = Yamlrw.of_string yaml in 162 + () 163 + with Yamlrw.Yamlrw_error _ -> ()); 164 + check true 165 + end 166 + else check true 167 + 168 + (** Test emitter with different scalar styles *) 169 + let () = 170 + add_test ~name:"emitter: scalar styles" [ printable_string ] @@ fun s -> 171 + let styles = 172 + [ `Any; `Plain; `Single_quoted; `Double_quoted; `Literal; `Folded ] 173 + in 174 + List.iter 175 + (fun style -> 176 + let emitter = Yamlrw.Stream.emitter () in 177 + (try 178 + Yamlrw.Stream.stream_start emitter `Utf8; 179 + Yamlrw.Stream.document_start emitter (); 180 + Yamlrw.Stream.scalar emitter ~style s; 181 + Yamlrw.Stream.document_end emitter (); 182 + Yamlrw.Stream.stream_end emitter; 183 + let yaml = Yamlrw.Stream.contents emitter in 184 + let _ = Yamlrw.of_string yaml in 185 + () 186 + with Yamlrw.Yamlrw_error _ -> ())) 187 + styles; 188 + check true 189 + 190 + (** Test emitter with anchors and aliases *) 191 + let () = 192 + add_test ~name:"emitter: anchors and aliases" [ ident_string; ident_string ] 193 + @@ fun anchor value -> 194 + if String.length anchor > 0 && String.length value > 0 then begin 195 + let emitter = Yamlrw.Stream.emitter () in 196 + (try 197 + Yamlrw.Stream.stream_start emitter `Utf8; 198 + Yamlrw.Stream.document_start emitter (); 199 + Yamlrw.Stream.mapping_start emitter (); 200 + Yamlrw.Stream.scalar emitter "original"; 201 + Yamlrw.Stream.scalar emitter ~anchor value; 202 + Yamlrw.Stream.scalar emitter "reference"; 203 + Yamlrw.Stream.alias emitter anchor; 204 + Yamlrw.Stream.mapping_end emitter; 205 + Yamlrw.Stream.document_end emitter (); 206 + Yamlrw.Stream.stream_end emitter; 207 + let yaml = Yamlrw.Stream.contents emitter in 208 + let _ = Yamlrw.of_string yaml in 209 + () 210 + with Yamlrw.Yamlrw_error _ -> ()); 211 + check true 212 + end 213 + else check true 214 + 215 + (** Test emitter with tags *) 216 + let () = 217 + add_test ~name:"emitter: tagged scalars" [ ident_string; ident_string ] 218 + @@ fun tag value -> 219 + if String.length value > 0 then begin 220 + let emitter = Yamlrw.Stream.emitter () in 221 + (try 222 + Yamlrw.Stream.stream_start emitter `Utf8; 223 + Yamlrw.Stream.document_start emitter (); 224 + Yamlrw.Stream.scalar emitter ~tag:("!" ^ tag) value; 225 + Yamlrw.Stream.document_end emitter (); 226 + Yamlrw.Stream.stream_end emitter; 227 + let yaml = Yamlrw.Stream.contents emitter in 228 + let _ = Yamlrw.yaml_of_string yaml in 229 + () 230 + with Yamlrw.Yamlrw_error _ -> ()); 231 + check true 232 + end 233 + else check true 234 + 235 + (** Test emitter with layout styles *) 236 + let () = 237 + add_test ~name:"emitter: layout styles" [ list ident_string ] @@ fun items -> 238 + if List.length items > 0 then begin 239 + let styles = [ `Any; `Block; `Flow ] in 240 + List.iter 241 + (fun style -> 242 + let emitter = Yamlrw.Stream.emitter () in 243 + (try 244 + Yamlrw.Stream.stream_start emitter `Utf8; 245 + Yamlrw.Stream.document_start emitter (); 246 + Yamlrw.Stream.sequence_start emitter ~style (); 247 + List.iter (fun s -> Yamlrw.Stream.scalar emitter s) items; 248 + Yamlrw.Stream.sequence_end emitter; 249 + Yamlrw.Stream.document_end emitter (); 250 + Yamlrw.Stream.stream_end emitter; 251 + let yaml = Yamlrw.Stream.contents emitter in 252 + let _ = Yamlrw.of_string yaml in 253 + () 254 + with Yamlrw.Yamlrw_error _ -> ())) 255 + styles; 256 + check true 257 + end 258 + else check true 259 + 260 + (** Test multiple documents *) 261 + let () = 262 + add_test ~name:"emitter: multiple documents" [ range 5; ident_string ] 263 + @@ fun count value -> 264 + if count > 0 && String.length value > 0 then begin 265 + let emitter = Yamlrw.Stream.emitter () in 266 + (try 267 + Yamlrw.Stream.stream_start emitter `Utf8; 268 + for i = 1 to count do 269 + Yamlrw.Stream.document_start emitter (); 270 + Yamlrw.Stream.scalar emitter (Printf.sprintf "%s%d" value i); 271 + Yamlrw.Stream.document_end emitter () 272 + done; 273 + Yamlrw.Stream.stream_end emitter; 274 + let yaml = Yamlrw.Stream.contents emitter in 275 + let docs = Yamlrw.documents_of_string yaml in 276 + if List.length docs <> count then fail "document count mismatch" 277 + else () 278 + with Yamlrw.Yamlrw_error _ -> ()); 279 + check true 280 + end 281 + else check true 282 + 283 + let run () = ()
+79
ocaml-yamlrw/fuzz/fuzz_encoding.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + (** Fuzz tests for Encoding module *) 7 + 8 + open Crowbar 9 + 10 + (** Test that encoding detection never crashes on arbitrary input *) 11 + let () = 12 + add_test ~name:"encoding: detect crash safety" [ bytes ] @@ fun buf -> 13 + let _ = Yamlrw.Encoding.detect buf in 14 + check true 15 + 16 + (** Test that to_string never crashes for any detected encoding *) 17 + let () = 18 + add_test ~name:"encoding: to_string after detect" [ bytes ] @@ fun buf -> 19 + let enc, _ = Yamlrw.Encoding.detect buf in 20 + let _ = Yamlrw.Encoding.to_string enc in 21 + check true 22 + 23 + (** Test that pp never crashes *) 24 + let () = 25 + add_test ~name:"encoding: pp after detect" [ bytes ] @@ fun buf -> 26 + let enc, _ = Yamlrw.Encoding.detect buf in 27 + let _ = Format.asprintf "%a" Yamlrw.Encoding.pp enc in 28 + check true 29 + 30 + (** Test encoding equality is reflexive *) 31 + let () = 32 + add_test ~name:"encoding: equal reflexive" [ bytes ] @@ fun buf -> 33 + let enc, _ = Yamlrw.Encoding.detect buf in 34 + if not (Yamlrw.Encoding.equal enc enc) then fail "encoding not equal to itself" 35 + else check true 36 + 37 + (** Test that BOM length is always non-negative and reasonable *) 38 + let () = 39 + add_test ~name:"encoding: bom_length non-negative" [ bytes ] @@ fun buf -> 40 + let _, bom_len = Yamlrw.Encoding.detect buf in 41 + if bom_len < 0 then fail "negative BOM length" 42 + else if bom_len > 4 then fail "BOM length too large (max 4 for UTF-32)" 43 + else check true 44 + 45 + (** Test specific BOM patterns *) 46 + let () = 47 + add_test ~name:"encoding: UTF-8 BOM" [ const () ] @@ fun () -> 48 + let utf8_bom = "\xEF\xBB\xBF" in 49 + let enc, len = Yamlrw.Encoding.detect utf8_bom in 50 + if enc <> `Utf8 then fail "expected UTF-8" 51 + else if len <> 3 then fail "expected BOM length 3" 52 + else check true 53 + 54 + let () = 55 + add_test ~name:"encoding: UTF-16 BE BOM" [ const () ] @@ fun () -> 56 + let utf16be_bom = "\xFE\xFF" in 57 + let enc, len = Yamlrw.Encoding.detect utf16be_bom in 58 + if enc <> `Utf16be then fail "expected UTF-16 BE" 59 + else if len <> 2 then fail "expected BOM length 2" 60 + else check true 61 + 62 + let () = 63 + add_test ~name:"encoding: UTF-16 LE BOM" [ const () ] @@ fun () -> 64 + (* Use BOM followed by non-null bytes to avoid ambiguity with UTF-32 LE *) 65 + let utf16le_bom = "\xFF\xFEab" in 66 + let enc, len = Yamlrw.Encoding.detect utf16le_bom in 67 + if enc <> `Utf16le then fail "expected UTF-16 LE" 68 + else if len <> 2 then fail "expected BOM length 2" 69 + else check true 70 + 71 + let () = 72 + add_test ~name:"encoding: empty string defaults to UTF-8" [ const () ] 73 + @@ fun () -> 74 + let enc, len = Yamlrw.Encoding.detect "" in 75 + if enc <> `Utf8 then fail "expected UTF-8 for empty string" 76 + else if len <> 0 then fail "expected BOM length 0 for empty string" 77 + else check true 78 + 79 + let run () = ()
+137
ocaml-yamlrw/fuzz/fuzz_tag.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + (** Fuzz tests for Tag module *) 7 + 8 + open Crowbar 9 + open Fuzz_common 10 + 11 + (** Test that of_string never crashes on arbitrary input *) 12 + let () = 13 + add_test ~name:"tag: of_string crash safety" [ bytes ] @@ fun buf -> 14 + let _ = Yamlrw.Tag.of_string buf in 15 + check true 16 + 17 + (** Test of_string/to_string roundtrip *) 18 + let () = 19 + add_test ~name:"tag: of_string/to_string roundtrip" [ bytes ] @@ fun buf -> 20 + match Yamlrw.Tag.of_string buf with 21 + | None -> check true (* Invalid tag, that's fine *) 22 + | Some tag -> 23 + let s = Yamlrw.Tag.to_string tag in 24 + (* Re-parse should succeed *) 25 + (match Yamlrw.Tag.of_string s with 26 + | None -> fail "re-parse of to_string output failed" 27 + | Some tag' -> 28 + if not (Yamlrw.Tag.equal tag tag') then fail "roundtrip mismatch" 29 + else check true) 30 + 31 + (** Test to_uri never crashes for valid tags *) 32 + let () = 33 + add_test ~name:"tag: to_uri after of_string" [ bytes ] @@ fun buf -> 34 + match Yamlrw.Tag.of_string buf with 35 + | None -> check true 36 + | Some tag -> 37 + let _ = Yamlrw.Tag.to_uri tag in 38 + check true 39 + 40 + (** Test pp never crashes *) 41 + let () = 42 + add_test ~name:"tag: pp" [ bytes ] @@ fun buf -> 43 + match Yamlrw.Tag.of_string buf with 44 + | None -> check true 45 + | Some tag -> 46 + let _ = Format.asprintf "%a" Yamlrw.Tag.pp tag in 47 + check true 48 + 49 + (** Test equality is reflexive *) 50 + let () = 51 + add_test ~name:"tag: equal reflexive" [ bytes ] @@ fun buf -> 52 + match Yamlrw.Tag.of_string buf with 53 + | None -> check true 54 + | Some tag -> 55 + if not (Yamlrw.Tag.equal tag tag) then fail "tag not equal to itself" 56 + else check true 57 + 58 + (** Test compare is antisymmetric *) 59 + let () = 60 + add_test ~name:"tag: compare antisymmetric" [ bytes; bytes ] 61 + @@ fun buf1 buf2 -> 62 + match (Yamlrw.Tag.of_string buf1, Yamlrw.Tag.of_string buf2) with 63 + | Some t1, Some t2 -> 64 + let cmp1 = Yamlrw.Tag.compare t1 t2 in 65 + let cmp2 = Yamlrw.Tag.compare t2 t1 in 66 + if cmp1 > 0 && cmp2 >= 0 then fail "compare not antisymmetric" 67 + else if cmp1 < 0 && cmp2 <= 0 then fail "compare not antisymmetric" 68 + else if cmp1 = 0 && cmp2 <> 0 then fail "compare not antisymmetric" 69 + else check true 70 + | _ -> check true 71 + 72 + (** Test make function *) 73 + let () = 74 + add_test ~name:"tag: make" [ ident_string; ident_string ] 75 + @@ fun handle suffix -> 76 + let tag = Yamlrw.Tag.make ~handle ~suffix in 77 + let _ = Yamlrw.Tag.to_string tag in 78 + let _ = Yamlrw.Tag.to_uri tag in 79 + check true 80 + 81 + (** Test standard tags exist and have expected properties *) 82 + let () = 83 + add_test ~name:"tag: standard tags" [ const () ] @@ fun () -> 84 + let tags = 85 + [ 86 + (Yamlrw.Tag.null, Yamlrw.Tag.is_null); 87 + (Yamlrw.Tag.bool, Yamlrw.Tag.is_bool); 88 + (Yamlrw.Tag.int, Yamlrw.Tag.is_int); 89 + (Yamlrw.Tag.float, Yamlrw.Tag.is_float); 90 + (Yamlrw.Tag.str, Yamlrw.Tag.is_str); 91 + (Yamlrw.Tag.seq, Yamlrw.Tag.is_seq); 92 + (Yamlrw.Tag.map, Yamlrw.Tag.is_map); 93 + ] 94 + in 95 + List.iter 96 + (fun (tag, pred) -> 97 + if not (pred tag) then fail "standard tag predicate failed" 98 + else 99 + let _ = Yamlrw.Tag.to_string tag in 100 + let _ = Yamlrw.Tag.to_uri tag in 101 + ()) 102 + tags; 103 + check true 104 + 105 + (** Test tag predicates are mutually exclusive for standard tags *) 106 + let () = 107 + add_test ~name:"tag: predicates mutually exclusive" [ const () ] @@ fun () -> 108 + let tags = 109 + [ 110 + Yamlrw.Tag.null; 111 + Yamlrw.Tag.bool; 112 + Yamlrw.Tag.int; 113 + Yamlrw.Tag.float; 114 + Yamlrw.Tag.str; 115 + Yamlrw.Tag.seq; 116 + Yamlrw.Tag.map; 117 + ] 118 + in 119 + let predicates = 120 + [ 121 + Yamlrw.Tag.is_null; 122 + Yamlrw.Tag.is_bool; 123 + Yamlrw.Tag.is_int; 124 + Yamlrw.Tag.is_float; 125 + Yamlrw.Tag.is_str; 126 + Yamlrw.Tag.is_seq; 127 + Yamlrw.Tag.is_map; 128 + ] 129 + in 130 + List.iter 131 + (fun tag -> 132 + let count = List.fold_left (fun n p -> if p tag then n + 1 else n) 0 predicates in 133 + if count <> 1 then fail "tag matched multiple predicates") 134 + tags; 135 + check true 136 + 137 + let run () = ()
+390
ocaml-yamlrw/fuzz/fuzz_value.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + (** Fuzz tests for Value module *) 7 + 8 + open Crowbar 9 + open Fuzz_common 10 + 11 + (** Generator for Value.t *) 12 + let rec value_gen depth = 13 + if depth <= 0 then 14 + choose 15 + [ 16 + const `Null; 17 + map [ bool ] (fun b -> `Bool b); 18 + map [ float ] (fun f -> `Float f); 19 + map [ printable_string ] (fun s -> `String s); 20 + ] 21 + else 22 + choose 23 + [ 24 + const `Null; 25 + map [ bool ] (fun b -> `Bool b); 26 + map [ float ] (fun f -> `Float f); 27 + map [ printable_string ] (fun s -> `String s); 28 + map [ list (value_gen (depth - 1)) ] (fun vs -> `A vs); 29 + map 30 + [ list (pair ident_string (value_gen (depth - 1))) ] 31 + (fun pairs -> `O pairs); 32 + ] 33 + 34 + let value = value_gen 3 35 + 36 + (** Test pp never crashes *) 37 + let () = 38 + add_test ~name:"value: pp" [ value ] @@ fun v -> 39 + let _ = Format.asprintf "%a" Yamlrw.Value.pp v in 40 + check true 41 + 42 + (** Test equal is reflexive *) 43 + let () = 44 + add_test ~name:"value: equal reflexive" [ value ] @@ fun v -> 45 + if not (Yamlrw.Value.equal v v) then fail "value not equal to itself" 46 + else check true 47 + 48 + (** Test compare is reflexive (returns 0 for same value) *) 49 + let () = 50 + add_test ~name:"value: compare reflexive" [ value ] @@ fun v -> 51 + if Yamlrw.Value.compare v v <> 0 then fail "compare should return 0 for same value" 52 + else check true 53 + 54 + (** Test type_name never crashes *) 55 + let () = 56 + add_test ~name:"value: type_name" [ value ] @@ fun v -> 57 + let _ = Yamlrw.Value.type_name v in 58 + check true 59 + 60 + (** Test safe accessors return correct types *) 61 + let () = 62 + add_test ~name:"value: as_null" [ value ] @@ fun v -> 63 + (match (v, Yamlrw.Value.as_null v) with 64 + | `Null, Some () -> () 65 + | `Null, None -> fail "as_null should return Some for Null" 66 + | _, Some () -> fail "as_null should return None for non-Null" 67 + | _, None -> ()); 68 + check true 69 + 70 + let () = 71 + add_test ~name:"value: as_bool" [ value ] @@ fun v -> 72 + (match (v, Yamlrw.Value.as_bool v) with 73 + | `Bool b, Some b' when b = b' -> () 74 + | `Bool _, Some _ -> fail "as_bool returned wrong value" 75 + | `Bool _, None -> fail "as_bool should return Some for Bool" 76 + | _, Some _ -> fail "as_bool should return None for non-Bool" 77 + | _, None -> ()); 78 + check true 79 + 80 + let () = 81 + add_test ~name:"value: as_float" [ value ] @@ fun v -> 82 + (match (v, Yamlrw.Value.as_float v) with 83 + | `Float f, Some f' when f = f' || (Float.is_nan f && Float.is_nan f') -> () 84 + | `Float _, Some _ -> fail "as_float returned wrong value" 85 + | `Float _, None -> fail "as_float should return Some for Float" 86 + | _, Some _ -> fail "as_float should return None for non-Float" 87 + | _, None -> ()); 88 + check true 89 + 90 + let () = 91 + add_test ~name:"value: as_string" [ value ] @@ fun v -> 92 + (match (v, Yamlrw.Value.as_string v) with 93 + | `String s, Some s' when s = s' -> () 94 + | `String _, Some _ -> fail "as_string returned wrong value" 95 + | `String _, None -> fail "as_string should return Some for String" 96 + | _, Some _ -> fail "as_string should return None for non-String" 97 + | _, None -> ()); 98 + check true 99 + 100 + let () = 101 + add_test ~name:"value: as_list" [ value ] @@ fun v -> 102 + (match (v, Yamlrw.Value.as_list v) with 103 + | `A lst, Some lst' when lst = lst' -> () 104 + | `A _, Some _ -> fail "as_list returned wrong value" 105 + | `A _, None -> fail "as_list should return Some for A" 106 + | _, Some _ -> fail "as_list should return None for non-A" 107 + | _, None -> ()); 108 + check true 109 + 110 + let () = 111 + add_test ~name:"value: as_assoc" [ value ] @@ fun v -> 112 + (match (v, Yamlrw.Value.as_assoc v) with 113 + | `O pairs, Some pairs' when pairs = pairs' -> () 114 + | `O _, Some _ -> fail "as_assoc returned wrong value" 115 + | `O _, None -> fail "as_assoc should return Some for O" 116 + | _, Some _ -> fail "as_assoc should return None for non-O" 117 + | _, None -> ()); 118 + check true 119 + 120 + (** Test constructors *) 121 + let () = 122 + add_test ~name:"value: null constructor" [ const () ] @@ fun () -> 123 + if Yamlrw.Value.null <> `Null then fail "null should be `Null" 124 + else check true 125 + 126 + let () = 127 + add_test ~name:"value: bool constructor" [ bool ] @@ fun b -> 128 + if Yamlrw.Value.bool b <> `Bool b then fail "bool constructor mismatch" 129 + else check true 130 + 131 + let () = 132 + add_test ~name:"value: int constructor" [ range 1000000 ] @@ fun n -> 133 + (* Use smaller range since floats can't exactly represent all int64 values *) 134 + match Yamlrw.Value.int n with 135 + | `Float f when Float.to_int f = n -> check true 136 + | `Float _ -> fail "int constructor roundtrip failed" 137 + | _ -> fail "int should produce Float" 138 + 139 + let () = 140 + add_test ~name:"value: float constructor" [ float ] @@ fun f -> 141 + match Yamlrw.Value.float f with 142 + | `Float f' when f = f' || (Float.is_nan f && Float.is_nan f') -> check true 143 + | `Float _ -> fail "float constructor roundtrip failed" 144 + | _ -> fail "float should produce Float" 145 + 146 + let () = 147 + add_test ~name:"value: string constructor" [ printable_string ] @@ fun s -> 148 + if Yamlrw.Value.string s <> `String s then fail "string constructor mismatch" 149 + else check true 150 + 151 + (** Test object operations *) 152 + let () = 153 + add_test ~name:"value: mem/find consistency" [ value; ident_string ] 154 + @@ fun v key -> 155 + match v with 156 + | `O _ -> 157 + let has_key = Yamlrw.Value.mem key v in 158 + let found = Yamlrw.Value.find key v in 159 + if has_key && Option.is_none found then fail "mem true but find None" 160 + else if (not has_key) && Option.is_some found then 161 + fail "mem false but find Some" 162 + else check true 163 + | _ -> check true 164 + 165 + (** Test map preserves structure *) 166 + let () = 167 + add_test ~name:"value: map preserves list length" [ value ] @@ fun v -> 168 + match v with 169 + | `A lst -> 170 + let mapped = Yamlrw.Value.map (fun x -> x) v in 171 + (match mapped with 172 + | `A lst' when List.length lst = List.length lst' -> check true 173 + | `A _ -> fail "map changed list length" 174 + | _ -> fail "map changed type") 175 + | _ -> check true 176 + 177 + (** Test combine for objects *) 178 + let () = 179 + add_test ~name:"value: combine objects" [ value; value ] @@ fun v1 v2 -> 180 + match (v1, v2) with 181 + | `O pairs1, `O pairs2 -> 182 + let combined = Yamlrw.Value.combine v1 v2 in 183 + (match combined with 184 + | `O pairs -> 185 + (* Combined should have all keys from both *) 186 + let keys1 = List.map fst pairs1 in 187 + let keys2 = List.map fst pairs2 in 188 + let all_keys = 189 + List.sort_uniq String.compare (keys1 @ keys2) 190 + in 191 + let combined_keys = 192 + List.sort_uniq String.compare (List.map fst pairs) 193 + in 194 + if all_keys = combined_keys then check true 195 + else fail "combine missing keys" 196 + | _ -> fail "combine should produce object") 197 + | _ -> check true 198 + 199 + (** Test generated value -> serialize -> parse roundtrip *) 200 + let () = 201 + add_test ~name:"value: generated value roundtrip" [ value ] @@ fun v -> 202 + (try 203 + let s = Yamlrw.to_string v in 204 + let v' = Yamlrw.of_string s in 205 + if not (Yamlrw.equal v v') then fail "generated value roundtrip mismatch" 206 + else () 207 + with Yamlrw.Yamlrw_error _ -> 208 + (* Some generated values might not roundtrip perfectly due to YAML ambiguities *) 209 + ()); 210 + check true 211 + 212 + (** Test generated value serialization with block style *) 213 + let () = 214 + add_test ~name:"value: generated block style" [ value ] @@ fun v -> 215 + (try 216 + let s = Yamlrw.to_string ~layout_style:`Block v in 217 + let _ = Yamlrw.of_string s in 218 + () 219 + with Yamlrw.Yamlrw_error _ -> ()); 220 + check true 221 + 222 + (** Test generated value serialization with flow style *) 223 + let () = 224 + add_test ~name:"value: generated flow style" [ value ] @@ fun v -> 225 + (try 226 + let s = Yamlrw.to_string ~layout_style:`Flow v in 227 + let _ = Yamlrw.of_string s in 228 + () 229 + with Yamlrw.Yamlrw_error _ -> ()); 230 + check true 231 + 232 + (** Test to_json/of_json roundtrip for generated values *) 233 + let () = 234 + add_test ~name:"value: to_json/of_json generated" [ value ] @@ fun v -> 235 + let y = Yamlrw.of_json v in 236 + let v' = Yamlrw.to_json y in 237 + if not (Yamlrw.equal v v') then fail "to_json/of_json roundtrip mismatch" 238 + else check true 239 + 240 + (** Test compare is transitive *) 241 + let () = 242 + add_test ~name:"value: compare transitive" [ value; value; value ] 243 + @@ fun v1 v2 v3 -> 244 + let c12 = Yamlrw.Value.compare v1 v2 in 245 + let c23 = Yamlrw.Value.compare v2 v3 in 246 + let c13 = Yamlrw.Value.compare v1 v3 in 247 + (* If v1 <= v2 and v2 <= v3 then v1 <= v3 *) 248 + if c12 <= 0 && c23 <= 0 && c13 > 0 then fail "compare not transitive" 249 + else if c12 >= 0 && c23 >= 0 && c13 < 0 then fail "compare not transitive" 250 + else check true 251 + 252 + (** Test equal is symmetric *) 253 + let () = 254 + add_test ~name:"value: equal symmetric" [ value; value ] @@ fun v1 v2 -> 255 + let eq12 = Yamlrw.Value.equal v1 v2 in 256 + let eq21 = Yamlrw.Value.equal v2 v1 in 257 + if eq12 <> eq21 then fail "equal not symmetric" else check true 258 + 259 + (** Test filter on lists *) 260 + let () = 261 + add_test ~name:"value: filter" [ value ] @@ fun v -> 262 + match v with 263 + | `A _ -> 264 + let filtered = Yamlrw.Value.filter (fun _ -> true) v in 265 + if not (Yamlrw.Value.equal v filtered) then 266 + fail "filter (fun _ -> true) should be identity" 267 + else 268 + let empty = Yamlrw.Value.filter (fun _ -> false) v in 269 + (match empty with 270 + | `A [] -> check true 271 + | `A _ -> fail "filter (fun _ -> false) should be empty" 272 + | _ -> fail "filter should preserve list type") 273 + | _ -> check true 274 + 275 + (** Test keys/values for objects *) 276 + let () = 277 + add_test ~name:"value: keys/values" [ value ] @@ fun v -> 278 + match v with 279 + | `O pairs -> 280 + let ks = Yamlrw.Value.keys v in 281 + let vs = Yamlrw.Value.values v in 282 + if List.length ks <> List.length pairs then fail "keys length mismatch" 283 + else if List.length vs <> List.length pairs then 284 + fail "values length mismatch" 285 + else check true 286 + | _ -> check true 287 + 288 + (** Test Util.update *) 289 + let () = 290 + add_test ~name:"value: Util.update" [ value; ident_string; value ] 291 + @@ fun v key newv -> 292 + match v with 293 + | `O _ -> 294 + (try 295 + let updated = Yamlrw.Util.update key newv v in 296 + let found = Yamlrw.Value.find key updated in 297 + match found with 298 + | Some x when Yamlrw.Value.equal x newv -> check true 299 + | Some _ -> fail "update: found wrong value" 300 + | None -> fail "update: key not found after update" 301 + with Yamlrw.Util.Type_error _ -> fail "Type_error on update") 302 + | _ -> check true 303 + 304 + (** Test Util.remove *) 305 + let () = 306 + add_test ~name:"value: Util.remove" [ value; ident_string ] @@ fun v key -> 307 + match v with 308 + | `O _ -> 309 + (try 310 + let removed = Yamlrw.Util.remove key v in 311 + let found = Yamlrw.Value.find key removed in 312 + if Option.is_some found then fail "remove: key still present" 313 + else check true 314 + with Yamlrw.Util.Type_error _ -> fail "Type_error on remove") 315 + | _ -> check true 316 + 317 + (** Test Util.get_path *) 318 + let () = 319 + add_test ~name:"value: Util.get_path" [ value; list ident_string ] 320 + @@ fun v path -> 321 + let _ = Yamlrw.Util.get_path path v in 322 + check true 323 + 324 + (** Test Util.flatten *) 325 + let () = 326 + add_test ~name:"value: Util.flatten" [ value ] @@ fun v -> 327 + match v with 328 + | `A _ -> 329 + (try 330 + let _ = Yamlrw.Util.flatten v in 331 + check true 332 + with Yamlrw.Util.Type_error _ -> fail "Type_error on flatten of list") 333 + | _ -> check true 334 + 335 + (** Test Util.nth *) 336 + let () = 337 + add_test ~name:"value: Util.nth" [ value; range 100 ] @@ fun v idx -> 338 + match v with 339 + | `A lst -> 340 + let result = Yamlrw.Util.nth idx v in 341 + if idx < List.length lst then 342 + match result with 343 + | Some x when Yamlrw.Value.equal x (List.nth lst idx) -> check true 344 + | Some _ -> fail "nth returned wrong element" 345 + | None -> fail "nth returned None for valid index" 346 + else if Option.is_some result then 347 + fail "nth returned Some for invalid index" 348 + else check true 349 + | _ -> check true 350 + 351 + (** Test Util.length *) 352 + let () = 353 + add_test ~name:"value: Util.length" [ value ] @@ fun v -> 354 + let len = Yamlrw.Util.length v in 355 + (match v with 356 + | `A lst when len = List.length lst -> () 357 + | `O pairs when len = List.length pairs -> () 358 + | `A _ -> fail "length mismatch for list" 359 + | `O _ -> fail "length mismatch for object" 360 + | _ when len = 0 -> () 361 + | _ -> fail "length should be 0 for scalars"); 362 + check true 363 + 364 + (** Test Util.fold *) 365 + let () = 366 + add_test ~name:"value: Util.fold" [ value ] @@ fun v -> 367 + match v with 368 + | `A lst -> 369 + (try 370 + let count = Yamlrw.Util.fold (fun acc _ -> acc + 1) 0 v in 371 + if count <> List.length lst then fail "fold count mismatch" 372 + else check true 373 + with Yamlrw.Util.Type_error _ -> fail "Type_error on fold of list") 374 + | _ -> check true 375 + 376 + (** Test Util.mapi preserves length *) 377 + let () = 378 + add_test ~name:"value: Util.mapi preserves length" [ value ] @@ fun v -> 379 + match v with 380 + | `A lst -> 381 + (try 382 + let mapped = Yamlrw.Util.mapi (fun _ x -> x) v in 383 + (match mapped with 384 + | `A lst' when List.length lst = List.length lst' -> check true 385 + | `A _ -> fail "mapi changed list length" 386 + | _ -> fail "mapi changed type") 387 + with Yamlrw.Util.Type_error _ -> fail "Type_error on mapi of list") 388 + | _ -> check true 389 + 390 + let run () = ()
+458
ocaml-yamlrw/fuzz/fuzz_yamlrw.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + (** Fuzz tests for the main Yamlrw parsing and serialization *) 7 + 8 + open Crowbar 9 + open Fuzz_common 10 + 11 + (** Test that of_string never crashes on arbitrary input *) 12 + let () = 13 + add_test ~name:"yamlrw: of_string crash safety" [ bytes ] @@ fun buf -> 14 + (try 15 + let _ = Yamlrw.of_string buf in 16 + () 17 + with Yamlrw.Yamlrw_error _ -> ()); 18 + check true 19 + 20 + (** Test that yaml_of_string never crashes on arbitrary input *) 21 + let () = 22 + add_test ~name:"yamlrw: yaml_of_string crash safety" [ bytes ] @@ fun buf -> 23 + (try 24 + let _ = Yamlrw.yaml_of_string buf in 25 + () 26 + with Yamlrw.Yamlrw_error _ -> ()); 27 + check true 28 + 29 + (** Test that documents_of_string never crashes on arbitrary input *) 30 + let () = 31 + add_test ~name:"yamlrw: documents_of_string crash safety" [ bytes ] 32 + @@ fun buf -> 33 + (try 34 + let _ = Yamlrw.documents_of_string buf in 35 + () 36 + with Yamlrw.Yamlrw_error _ -> ()); 37 + check true 38 + 39 + (** Test roundtrip: parse -> serialize -> parse should give equal values *) 40 + let () = 41 + add_test ~name:"yamlrw: value roundtrip" [ bytes ] @@ fun buf -> 42 + match 43 + try Some (Yamlrw.of_string buf) with Yamlrw.Yamlrw_error _ -> None 44 + with 45 + | None -> check true (* Invalid input is fine *) 46 + | Some v1 -> 47 + let serialized = Yamlrw.to_string v1 in 48 + (match 49 + try Some (Yamlrw.of_string serialized) 50 + with Yamlrw.Yamlrw_error _ -> None 51 + with 52 + | None -> fail "re-parse of serialized output failed" 53 + | Some v2 -> 54 + if not (Yamlrw.equal v1 v2) then fail "roundtrip mismatch" 55 + else check true) 56 + 57 + (** Test yaml roundtrip - serializing and re-parsing should not crash. 58 + Note: We don't check for value equality because YAML has ambiguous 59 + edge cases (e.g., strings ending in ':' can be re-parsed as mapping keys). *) 60 + let () = 61 + add_test ~name:"yamlrw: yaml roundtrip" [ bytes ] @@ fun buf -> 62 + match 63 + try Some (Yamlrw.yaml_of_string ~resolve_aliases:true buf) 64 + with Yamlrw.Yamlrw_error _ -> None 65 + with 66 + | None -> check true 67 + | Some y1 -> 68 + let serialized = Yamlrw.yaml_to_string y1 in 69 + (match 70 + try Some (Yamlrw.yaml_of_string ~resolve_aliases:true serialized) 71 + with Yamlrw.Yamlrw_error _ -> None 72 + with 73 + | None -> fail "re-parse of serialized yaml failed" 74 + | Some _y2 -> 75 + (* Just verify it parses - don't check equality due to YAML ambiguities *) 76 + check true) 77 + 78 + (** Test to_string never crashes for valid parsed values *) 79 + let () = 80 + add_test ~name:"yamlrw: to_string after of_string" [ bytes ] @@ fun buf -> 81 + (try 82 + let v = Yamlrw.of_string buf in 83 + let _ = Yamlrw.to_string v in 84 + () 85 + with Yamlrw.Yamlrw_error _ -> ()); 86 + check true 87 + 88 + (** Test pp never crashes *) 89 + let () = 90 + add_test ~name:"yamlrw: pp" [ bytes ] @@ fun buf -> 91 + (try 92 + let v = Yamlrw.of_string buf in 93 + let _ = Format.asprintf "%a" Yamlrw.pp v in 94 + () 95 + with Yamlrw.Yamlrw_error _ -> ()); 96 + check true 97 + 98 + (** Test equal is reflexive for parsed values *) 99 + let () = 100 + add_test ~name:"yamlrw: equal reflexive" [ bytes ] @@ fun buf -> 101 + (try 102 + let v = Yamlrw.of_string buf in 103 + if not (Yamlrw.equal v v) then fail "value not equal to itself" else () 104 + with Yamlrw.Yamlrw_error _ -> ()); 105 + check true 106 + 107 + (** Test of_json/to_json roundtrip *) 108 + let () = 109 + add_test ~name:"yamlrw: of_json/to_json roundtrip" [ bytes ] @@ fun buf -> 110 + (try 111 + let v = Yamlrw.of_string buf in 112 + let y = Yamlrw.of_json v in 113 + let v' = Yamlrw.to_json y in 114 + if not (Yamlrw.equal v v') then fail "of_json/to_json roundtrip mismatch" 115 + else () 116 + with Yamlrw.Yamlrw_error _ -> ()); 117 + check true 118 + 119 + (** Test serialization with different styles *) 120 + let () = 121 + add_test ~name:"yamlrw: to_string with block style" [ bytes ] @@ fun buf -> 122 + (try 123 + let v = Yamlrw.of_string buf in 124 + let _ = Yamlrw.to_string ~layout_style:`Block v in 125 + () 126 + with Yamlrw.Yamlrw_error _ -> ()); 127 + check true 128 + 129 + let () = 130 + add_test ~name:"yamlrw: to_string with flow style" [ bytes ] @@ fun buf -> 131 + (try 132 + let v = Yamlrw.of_string buf in 133 + let _ = Yamlrw.to_string ~layout_style:`Flow v in 134 + () 135 + with Yamlrw.Yamlrw_error _ -> ()); 136 + check true 137 + 138 + (** Test simple valid YAML strings parse correctly *) 139 + let () = 140 + add_test ~name:"yamlrw: simple string" [ printable_string ] @@ fun s -> 141 + (* Wrap in quotes to ensure it's a valid YAML string *) 142 + let yaml = "\"" ^ String.escaped s ^ "\"" in 143 + (try 144 + let _ = Yamlrw.of_string yaml in 145 + () 146 + with Yamlrw.Yamlrw_error _ -> ()); 147 + check true 148 + 149 + (** Test simple key-value mapping *) 150 + let () = 151 + add_test ~name:"yamlrw: key-value mapping" [ ident_string; ident_string ] 152 + @@ fun key value -> 153 + if String.length key > 0 && String.length value > 0 then begin 154 + let yaml = key ^ ": " ^ value in 155 + match 156 + try Some (Yamlrw.of_string yaml) with Yamlrw.Yamlrw_error _ -> None 157 + with 158 + | None -> check true 159 + | Some v -> 160 + (match v with 161 + | `O [ (k, `String _) ] when k = key -> check true 162 + | `O [ (k, `Float _) ] when k = key -> check true 163 + | `O [ (k, `Bool _) ] when k = key -> check true 164 + | `O [ (k, `Null) ] when k = key -> check true 165 + | _ -> check true) 166 + end 167 + else check true 168 + 169 + (** Test sequence parsing *) 170 + let () = 171 + add_test ~name:"yamlrw: sequence" [ list ident_string ] @@ fun items -> 172 + if List.length items > 0 && List.for_all (fun s -> String.length s > 0) items 173 + then begin 174 + let yaml = String.concat "\n" (List.map (fun s -> "- " ^ s) items) in 175 + (try 176 + let v = Yamlrw.of_string yaml in 177 + match v with 178 + | `A lst when List.length lst = List.length items -> () 179 + | `A _ -> fail "sequence length mismatch" 180 + | _ -> fail "expected sequence" 181 + with Yamlrw.Yamlrw_error _ -> ()); 182 + check true 183 + end 184 + else check true 185 + 186 + (** Test document boundaries *) 187 + let () = 188 + add_test ~name:"yamlrw: document markers" [ const () ] @@ fun () -> 189 + let yaml = "---\nfoo: bar\n...\n---\nbaz: qux\n..." in 190 + (try 191 + let docs = Yamlrw.documents_of_string yaml in 192 + if List.length docs <> 2 then fail "expected 2 documents" else () 193 + with Yamlrw.Yamlrw_error _ -> ()); 194 + check true 195 + 196 + (** Test alias limits are enforced *) 197 + let () = 198 + add_test ~name:"yamlrw: alias depth limit" [ const () ] @@ fun () -> 199 + (* Create deeply nested alias structure *) 200 + let yaml = "&a [*a]" in 201 + (try 202 + let _ = Yamlrw.of_string ~max_depth:5 yaml in 203 + () 204 + with Yamlrw.Yamlrw_error _ -> ()); 205 + check true 206 + 207 + (** Test buffer-based parsing *) 208 + let () = 209 + add_test ~name:"yamlrw: of_buffer crash safety" [ bytes ] @@ fun buf -> 210 + let buffer = Buffer.create (String.length buf) in 211 + Buffer.add_string buffer buf; 212 + (try 213 + let _ = Yamlrw.of_buffer buffer in 214 + () 215 + with Yamlrw.Yamlrw_error _ -> ()); 216 + check true 217 + 218 + (** Test to_buffer produces parseable output *) 219 + let () = 220 + add_test ~name:"yamlrw: to_buffer roundtrip" [ bytes ] @@ fun buf -> 221 + (try 222 + let v = Yamlrw.of_string buf in 223 + let buffer = Yamlrw.to_buffer v in 224 + let v' = Yamlrw.of_buffer buffer in 225 + if not (Yamlrw.equal v v') then fail "to_buffer roundtrip mismatch" else () 226 + with Yamlrw.Yamlrw_error _ -> ()); 227 + check true 228 + 229 + (** Test double roundtrip stabilizes - serialize twice should be identical *) 230 + let () = 231 + add_test ~name:"yamlrw: double roundtrip stabilizes" [ bytes ] @@ fun buf -> 232 + (try 233 + let v1 = Yamlrw.of_string buf in 234 + let s1 = Yamlrw.to_string v1 in 235 + let v2 = Yamlrw.of_string s1 in 236 + let s2 = Yamlrw.to_string v2 in 237 + let v3 = Yamlrw.of_string s2 in 238 + let s3 = Yamlrw.to_string v3 in 239 + (* After two roundtrips, serialization should stabilize *) 240 + if s2 <> s3 then fail "serialization did not stabilize after 2 roundtrips" 241 + else if not (Yamlrw.equal v2 v3) then fail "values differ after stabilization" 242 + else () 243 + with Yamlrw.Yamlrw_error _ -> ()); 244 + check true 245 + 246 + (** Test cross-style roundtrip: parse any, emit block, re-parse *) 247 + let () = 248 + add_test ~name:"yamlrw: cross-style block roundtrip" [ bytes ] @@ fun buf -> 249 + (try 250 + let v1 = Yamlrw.of_string buf in 251 + let s_block = Yamlrw.to_string ~layout_style:`Block v1 in 252 + let v2 = Yamlrw.of_string s_block in 253 + if not (Yamlrw.equal v1 v2) then fail "block style roundtrip mismatch" 254 + else () 255 + with Yamlrw.Yamlrw_error _ -> ()); 256 + check true 257 + 258 + (** Test cross-style roundtrip: parse any, emit flow, re-parse *) 259 + let () = 260 + add_test ~name:"yamlrw: cross-style flow roundtrip" [ bytes ] @@ fun buf -> 261 + (try 262 + let v1 = Yamlrw.of_string buf in 263 + let s_flow = Yamlrw.to_string ~layout_style:`Flow v1 in 264 + let v2 = Yamlrw.of_string s_flow in 265 + if not (Yamlrw.equal v1 v2) then fail "flow style roundtrip mismatch" 266 + else () 267 + with Yamlrw.Yamlrw_error _ -> ()); 268 + check true 269 + 270 + (** Test scanner never crashes on arbitrary input *) 271 + let () = 272 + add_test ~name:"yamlrw: scanner crash safety" [ bytes ] @@ fun buf -> 273 + (try 274 + let scanner = Yamlrw.Scanner.of_string buf in 275 + let _ = Yamlrw.Scanner.to_list scanner in 276 + () 277 + with Yamlrw.Yamlrw_error _ -> ()); 278 + check true 279 + 280 + (** Test streaming parser never crashes *) 281 + let () = 282 + add_test ~name:"yamlrw: stream parser crash safety" [ bytes ] @@ fun buf -> 283 + (try 284 + let parser = Yamlrw.Stream.parser buf in 285 + Yamlrw.Stream.iter (fun _ _ _ -> ()) parser 286 + with Yamlrw.Yamlrw_error _ -> ()); 287 + check true 288 + 289 + (** Test that scanner tokens and parser events are consistent *) 290 + let () = 291 + add_test ~name:"yamlrw: scanner/parser consistency" [ bytes ] @@ fun buf -> 292 + let scanner_ok = 293 + try 294 + let scanner = Yamlrw.Scanner.of_string buf in 295 + let _ = Yamlrw.Scanner.to_list scanner in 296 + true 297 + with Yamlrw.Yamlrw_error _ -> false 298 + in 299 + let parser_ok = 300 + try 301 + let parser = Yamlrw.Stream.parser buf in 302 + Yamlrw.Stream.iter (fun _ _ _ -> ()) parser; 303 + true 304 + with Yamlrw.Yamlrw_error _ -> false 305 + in 306 + (* If scanner succeeds, parser should not crash (may still error on invalid structure) *) 307 + if scanner_ok && not parser_ok then 308 + (* This is actually OK - scanner can tokenize invalid YAML structure *) 309 + check true 310 + else check true 311 + 312 + (** Test literal block scalar style *) 313 + let () = 314 + add_test ~name:"yamlrw: literal block scalar" [ printable_string ] @@ fun s -> 315 + if String.length s > 0 then begin 316 + let yaml = "|\n " ^ String.concat "\n " (String.split_on_char '\n' s) in 317 + (try 318 + let _ = Yamlrw.of_string yaml in 319 + () 320 + with Yamlrw.Yamlrw_error _ -> ()); 321 + check true 322 + end 323 + else check true 324 + 325 + (** Test folded block scalar style *) 326 + let () = 327 + add_test ~name:"yamlrw: folded block scalar" [ printable_string ] @@ fun s -> 328 + if String.length s > 0 then begin 329 + let yaml = ">\n " ^ String.concat "\n " (String.split_on_char '\n' s) in 330 + (try 331 + let _ = Yamlrw.of_string yaml in 332 + () 333 + with Yamlrw.Yamlrw_error _ -> ()); 334 + check true 335 + end 336 + else check true 337 + 338 + (** Test single-quoted scalar *) 339 + let () = 340 + add_test ~name:"yamlrw: single quoted scalar" [ printable_string ] @@ fun s -> 341 + (* Escape single quotes by doubling them *) 342 + let escaped = Str.global_replace (Str.regexp "'") "''" s in 343 + let yaml = "'" ^ escaped ^ "'" in 344 + (try 345 + let _ = Yamlrw.of_string yaml in 346 + () 347 + with Yamlrw.Yamlrw_error _ -> ()); 348 + check true 349 + 350 + (** Test double-quoted scalar with escape sequences *) 351 + let () = 352 + add_test ~name:"yamlrw: double quoted with escapes" [ printable_string ] 353 + @@ fun s -> 354 + let yaml = "\"" ^ String.escaped s ^ "\"" in 355 + (try 356 + let _ = Yamlrw.of_string yaml in 357 + () 358 + with Yamlrw.Yamlrw_error _ -> ()); 359 + check true 360 + 361 + (** Test deeply nested structures don't crash *) 362 + let () = 363 + add_test ~name:"yamlrw: deep nesting" [ range 50 ] @@ fun depth -> 364 + let yaml = String.make depth '[' ^ "null" ^ String.make depth ']' in 365 + (try 366 + let _ = Yamlrw.of_string yaml in 367 + () 368 + with Yamlrw.Yamlrw_error _ -> ()); 369 + check true 370 + 371 + (** Test multiple anchors and aliases *) 372 + let () = 373 + add_test ~name:"yamlrw: multiple anchors" [ ident_string; ident_string ] 374 + @@ fun name1 name2 -> 375 + if String.length name1 > 0 && String.length name2 > 0 then begin 376 + let yaml = 377 + Printf.sprintf "a: &%s value1\nb: &%s value2\nc: *%s\nd: *%s" name1 name2 378 + name1 name2 379 + in 380 + (try 381 + let _ = Yamlrw.of_string yaml in 382 + () 383 + with Yamlrw.Yamlrw_error _ -> ()); 384 + check true 385 + end 386 + else check true 387 + 388 + (** Test error positions are within input bounds *) 389 + let () = 390 + add_test ~name:"yamlrw: error position bounds" [ bytes ] @@ fun buf -> 391 + (try 392 + let _ = Yamlrw.of_string buf in 393 + () 394 + with Yamlrw.Yamlrw_error err -> 395 + (* Error has span : Span.t option, and Span has start/end positions *) 396 + match err.span with 397 + | None -> () (* No position info, that's ok *) 398 + | Some span -> 399 + let start_pos = span.start in 400 + let line = start_pos.Yamlrw.Position.line in 401 + let col = start_pos.Yamlrw.Position.column in 402 + let offset = start_pos.Yamlrw.Position.index in 403 + if line < 1 then fail "error line < 1" 404 + else if col < 0 then fail "error column < 0" 405 + else if offset < 0 then fail "error offset < 0" 406 + else if offset > String.length buf then 407 + fail "error offset > input length" 408 + else ()); 409 + check true 410 + 411 + (** Test yaml_of_string with resolve_aliases=true vs false *) 412 + let () = 413 + add_test ~name:"yamlrw: yaml resolve_aliases modes" [ bytes ] @@ fun buf -> 414 + let with_resolve = 415 + try Some (Yamlrw.yaml_of_string ~resolve_aliases:true buf) 416 + with Yamlrw.Yamlrw_error _ -> None 417 + in 418 + let without_resolve = 419 + try Some (Yamlrw.yaml_of_string ~resolve_aliases:false buf) 420 + with Yamlrw.Yamlrw_error _ -> None 421 + in 422 + (* Both should either succeed or fail, but not crash *) 423 + (match (with_resolve, without_resolve) with 424 + | Some y1, Some _y2 -> 425 + (* If both succeed, serializing resolved version should work *) 426 + let _ = Yamlrw.yaml_to_string y1 in 427 + () 428 + | _ -> ()); 429 + check true 430 + 431 + (** Test documents roundtrip with resolve_aliases=false preserves structure *) 432 + let () = 433 + add_test ~name:"yamlrw: documents roundtrip (no resolve)" [ bytes ] @@ fun buf -> 434 + (try 435 + let docs = Yamlrw.documents_of_string buf in 436 + let serialized = Yamlrw.documents_to_string ~resolve_aliases:false docs in 437 + let docs' = Yamlrw.documents_of_string serialized in 438 + if List.length docs <> List.length docs' then 439 + fail "document count mismatch after roundtrip (no resolve)" 440 + else () 441 + with Yamlrw.Yamlrw_error _ -> ()); 442 + check true 443 + 444 + (** Test documents roundtrip with resolve_aliases=true *) 445 + let () = 446 + add_test ~name:"yamlrw: documents roundtrip (resolve)" [ bytes ] @@ fun buf -> 447 + (try 448 + let docs = Yamlrw.documents_of_string buf in 449 + let serialized = Yamlrw.documents_to_string ~resolve_aliases:true docs in 450 + (* With resolve_aliases=true, anchors are stripped. Empty scalars with 451 + only anchors become truly empty, which may reduce document count. 452 + We just verify re-parsing doesn't crash. *) 453 + let _ = Yamlrw.documents_of_string serialized in 454 + () 455 + with Yamlrw.Yamlrw_error _ -> ()); 456 + check true 457 + 458 + let run () = ()
+11
ocaml-yamlrw/fuzz/input/anchor_alias
··· 1 + defaults: &defaults 2 + timeout: 30 3 + retries: 3 4 + 5 + production: 6 + <<: *defaults 7 + host: prod.example.com 8 + 9 + staging: 10 + <<: *defaults 11 + host: stage.example.com
+1
ocaml-yamlrw/fuzz/input/bool_false
··· 1 + false
+1
ocaml-yamlrw/fuzz/input/bool_true
··· 1 + true
+2
ocaml-yamlrw/fuzz/input/comment
··· 1 + # This is a comment 2 + key: value # inline comment
+2
ocaml-yamlrw/fuzz/input/double_quoted
··· 1 + double quoted 2 + with escapes tab
ocaml-yamlrw/fuzz/input/empty

This is a binary file and will not be displayed.

+1
ocaml-yamlrw/fuzz/input/float
··· 1 + 3.14159
+1
ocaml-yamlrw/fuzz/input/flow_mapping
··· 1 + {name: Alice, age: 30}
+1
ocaml-yamlrw/fuzz/input/flow_sequence
··· 1 + [1, 2, 3, 4, 5]
+4
ocaml-yamlrw/fuzz/input/folded_block
··· 1 + description: > 2 + This is a folded 3 + block scalar that 4 + folds newlines.
+1
ocaml-yamlrw/fuzz/input/integer
··· 1 + 42
+4
ocaml-yamlrw/fuzz/input/literal_block
··· 1 + description: | 2 + This is a literal 3 + block scalar that 4 + preserves newlines.
+3
ocaml-yamlrw/fuzz/input/mapping
··· 1 + name: Alice 2 + age: 30 3 + active: true
+6
ocaml-yamlrw/fuzz/input/multi_document
··· 1 + --- 2 + first: document 3 + ... 4 + --- 5 + second: document 6 + ...
+8
ocaml-yamlrw/fuzz/input/nested
··· 1 + person: 2 + name: Bob 3 + address: 4 + city: London 5 + zip: "12345" 6 + hobbies: 7 + - reading 8 + - cycling
+1
ocaml-yamlrw/fuzz/input/null
··· 1 + null
+3
ocaml-yamlrw/fuzz/input/sequence
··· 1 + - apple 2 + - banana 3 + - cherry
+1
ocaml-yamlrw/fuzz/input/single_quoted
··· 1 + 'single quoted: with colon'
+1
ocaml-yamlrw/fuzz/input/string
··· 1 + hello world
+1
ocaml-yamlrw/fuzz/input/tagged
··· 1 + !!str 123
+3
ocaml-yamlrw/fuzz/input/version
··· 1 + %YAML 1.2 2 + --- 3 + key: value
+59 -23
ocaml-yamlrw/lib/emitter.ml
··· 169 169 Buffer.contents buf 170 170 end 171 171 172 - (** Write scalar with appropriate quoting *) 172 + (** Write indentation for block scalar content. 173 + Block scalar content must be indented by at least 1 space more than the 174 + containing structure. We use config.indent spaces, ensuring at least 1. *) 175 + let write_block_scalar_indent t = 176 + let content_indent = max 1 t.config.indent in 177 + for _ = 1 to t.indent + content_indent do 178 + write_char t ' ' 179 + done 180 + 181 + (** Write scalar with appropriate quoting. 182 + Returns true if the scalar ends with a newline (block scalars), false otherwise. 183 + Callers should check this to avoid double newlines. *) 173 184 let write_scalar t ?(style = `Any) value = 174 185 match match style with `Any -> Quoting.choose_style value | s -> s with 175 - | `Plain | `Any -> write t value 186 + | `Plain | `Any -> 187 + write t value; 188 + false 176 189 | `Single_quoted -> 177 190 write_char t '\''; 178 191 write t (escape_single_quoted value); 179 - write_char t '\'' 192 + write_char t '\''; 193 + false 180 194 | `Double_quoted -> 181 195 write_char t '"'; 182 196 write t (escape_double_quoted value); 183 - write_char t '"' 197 + write_char t '"'; 198 + false 184 199 | `Literal -> 185 200 write t "|"; 186 201 write_newline t; 187 - String.split_on_char '\n' value 188 - |> List.iter (fun line -> 189 - write_indent t; 190 - write t line; 191 - write_newline t) 202 + let lines = String.split_on_char '\n' value in 203 + let rec write_lines = function 204 + | [] -> () 205 + | [ last ] -> 206 + write_block_scalar_indent t; 207 + write t last 208 + (* No trailing newline - caller will add it *) 209 + | line :: rest -> 210 + write_block_scalar_indent t; 211 + write t line; 212 + write_newline t; 213 + write_lines rest 214 + in 215 + write_lines lines; 216 + true (* Block scalar ends with content on last line, needs newline from caller *) 192 217 | `Folded -> 193 218 write t ">"; 194 219 write_newline t; 195 - String.split_on_char '\n' value 196 - |> List.iter (fun line -> 197 - write_indent t; 198 - write t line; 199 - write_newline t) 220 + let lines = String.split_on_char '\n' value in 221 + let rec write_lines = function 222 + | [] -> () 223 + | [ last ] -> 224 + write_block_scalar_indent t; 225 + write t last 226 + (* No trailing newline - caller will add it *) 227 + | line :: rest -> 228 + write_block_scalar_indent t; 229 + write t line; 230 + write_newline t; 231 + write_lines rest 232 + in 233 + write_lines lines; 234 + true (* Block scalar ends with content on last line, needs newline from caller *) 200 235 201 236 (** Write anchor if present *) 202 237 let write_anchor t anchor = ··· 275 310 if t.need_separator then write t ", "; 276 311 write_anchor t anchor; 277 312 write_tag t ~implicit:plain_implicit tag; 278 - write_scalar t ~style value; 313 + let (_ : bool) = write_scalar t ~style value in 279 314 write t ": "; 280 315 t.need_separator <- false; 281 316 t.state <- In_flow_mapping_value ··· 286 321 write t ", "; 287 322 write_anchor t anchor; 288 323 write_tag t ~implicit:plain_implicit tag; 289 - write_scalar t ~style value; 324 + let (_ : bool) = write_scalar t ~style value in 290 325 write t ": "; 291 326 t.need_separator <- false; 292 327 t.state <- In_flow_mapping_value ··· 295 330 (* Normal value scalar *) 296 331 write_anchor t anchor; 297 332 write_tag t ~implicit:plain_implicit tag; 298 - write_scalar t ~style value; 333 + let (_ : bool) = write_scalar t ~style value in 299 334 t.need_separator <- true; 300 335 t.state <- In_flow_mapping_key 301 336 end ··· 304 339 t.need_separator <- true; 305 340 write_anchor t anchor; 306 341 write_tag t ~implicit:plain_implicit tag; 307 - write_scalar t ~style value 342 + let (_ : bool) = write_scalar t ~style value in 343 + () 308 344 end 309 345 else begin 310 346 match t.state with ··· 313 349 write t "- "; 314 350 write_anchor t anchor; 315 351 write_tag t ~implicit:plain_implicit tag; 316 - write_scalar t ~style value; 352 + let (_ : bool) = write_scalar t ~style value in 317 353 write_newline t 318 354 | In_block_mapping_key indent -> 319 355 write_indent t; 320 356 write_anchor t anchor; 321 357 write_tag t ~implicit:plain_implicit tag; 322 - write_scalar t ~style value; 358 + let (_ : bool) = write_scalar t ~style value in 323 359 write_char t ':'; 324 360 t.state <- In_block_mapping_value indent 325 361 | In_block_mapping_first_key indent -> 326 362 (* First key after "- ", no indent needed *) 327 363 write_anchor t anchor; 328 364 write_tag t ~implicit:plain_implicit tag; 329 - write_scalar t ~style value; 365 + let (_ : bool) = write_scalar t ~style value in 330 366 write_char t ':'; 331 367 t.state <- In_block_mapping_value indent 332 368 | In_block_mapping_value indent -> 333 369 write_char t ' '; 334 370 write_anchor t anchor; 335 371 write_tag t ~implicit:plain_implicit tag; 336 - write_scalar t ~style value; 372 + let (_ : bool) = write_scalar t ~style value in 337 373 write_newline t; 338 374 t.state <- In_block_mapping_key indent 339 375 | _ -> 340 376 write_anchor t anchor; 341 377 write_tag t ~implicit:plain_implicit tag; 342 - write_scalar t ~style value; 378 + let (_ : bool) = write_scalar t ~style value in 343 379 write_newline t 344 380 end 345 381 | Event.Sequence_start { anchor; tag; implicit; style } ->
+3
ocaml-yamlrw/lib/scanner.ml
··· 534 534 | _ -> Error.raise_at start (Invalid_hex_escape (Buffer.contents buf)) 535 535 done; 536 536 let code = int_of_string ("0x" ^ Buffer.contents buf) in 537 + (* Validate Unicode scalar value (0x0000-0x10FFFF, excluding surrogates) *) 538 + if not (Uchar.is_valid code) then 539 + Error.raise_at start (Invalid_unicode_escape (Buffer.contents buf)); 537 540 if code <= 0x7F then String.make 1 (Char.chr code) 538 541 else if code <= 0x7FF then 539 542 let b1 = 0xC0 lor (code lsr 6) in
+17 -23
ocaml-yamlrw/lib/yaml.ml
··· 120 120 v 121 121 | `Alias name -> expand_alias ~depth name 122 122 | `A seq -> 123 - (* First resolve all members in order *) 123 + (* Register anchor with ORIGINAL node BEFORE resolving members. 124 + This ensures that when this anchor is expanded later through 125 + an alias chain, the internal aliases still need resolution, 126 + allowing the depth counter to properly accumulate. *) 127 + Option.iter (fun name -> register_anchor name v) (Sequence.anchor seq); 128 + (* Now resolve all members in order *) 124 129 let resolved_members = 125 130 List.map (resolve ~depth) (Sequence.members seq) 126 131 in 127 - let resolved = 128 - `A 129 - (Sequence.make ?anchor:(Sequence.anchor seq) ?tag:(Sequence.tag seq) 130 - ~implicit:(Sequence.implicit seq) ~style:(Sequence.style seq) 131 - resolved_members) 132 - in 133 - (* Register anchor with resolved node *) 134 - Option.iter 135 - (fun name -> register_anchor name resolved) 136 - (Sequence.anchor seq); 137 - resolved 132 + `A 133 + (Sequence.make ?anchor:(Sequence.anchor seq) ?tag:(Sequence.tag seq) 134 + ~implicit:(Sequence.implicit seq) ~style:(Sequence.style seq) 135 + resolved_members) 138 136 | `O map -> 137 + (* Register anchor with ORIGINAL node BEFORE resolving members. 138 + This ensures proper depth tracking for alias chains. *) 139 + Option.iter (fun name -> register_anchor name v) (Mapping.anchor map); 139 140 (* Process key-value pairs in document order *) 140 141 let resolved_pairs = 141 142 List.map ··· 145 146 (resolved_k, resolved_v)) 146 147 (Mapping.members map) 147 148 in 148 - let resolved = 149 - `O 150 - (Mapping.make ?anchor:(Mapping.anchor map) ?tag:(Mapping.tag map) 151 - ~implicit:(Mapping.implicit map) ~style:(Mapping.style map) 152 - resolved_pairs) 153 - in 154 - (* Register anchor with resolved node *) 155 - Option.iter 156 - (fun name -> register_anchor name resolved) 157 - (Mapping.anchor map); 158 - resolved 149 + `O 150 + (Mapping.make ?anchor:(Mapping.anchor map) ?tag:(Mapping.tag map) 151 + ~implicit:(Mapping.implicit map) ~style:(Mapping.style map) 152 + resolved_pairs) 159 153 in 160 154 resolve ~depth:0 root 161 155
+1 -1
ocaml-yamlrw/tests/cram/bomb.t
··· 14 14 Test depth limit with a nested alias chain: 15 15 16 16 $ yamlcat --max-depth 2 --json depth_bomb.yml | head -c 50 17 - {"a": ["x", "y", "z"], "b": [["x", "y", "z"], ["x" 17 + Error: alias expansion exceeded depth limit (2 levels) 18 18 19 19 $ yamlcat --max-depth 10 --json depth_bomb.yml | head -c 50 20 20 {"a": ["x", "y", "z"], "b": [["x", "y", "z"], ["x"
+8 -2
ocaml-yamlrw/tests/dune
··· 2 2 3 3 (executable 4 4 (name run_all_tests) 5 - (modules run_all_tests test_yamlrw) 6 - (libraries yamlrw test_suite_lib alcotest)) 5 + (modules run_all_tests) 6 + (libraries yamlrw test_suite_lib)) 7 + 8 + ; Unit tests using Alcotest 9 + (test 10 + (name test_yamlrw) 11 + (modules test_yamlrw) 12 + (libraries yamlrw alcotest)) 7 13 8 14 (executable 9 15 (name run_all_tests_eio)
+97 -1
ocaml-yamlrw/tests/test_yamlrw.ml
··· 275 275 | `O [ ("description", `String _) ] -> () 276 276 | _ -> Alcotest.fail "expected mapping with folded block" 277 277 278 + (* Test that block scalars don't create double newlines when emitted as values. 279 + This was a bug where write_scalar would add a trailing newline for block 280 + scalars, and then the caller would also add a newline, creating a blank line 281 + between the value and the next key. *) 282 + let test_block_scalar_no_double_newline () = 283 + (* Create a value that will use folded style due to length > 80 chars, 284 + or explicitly use events to force block scalar style *) 285 + let emitter = Emitter.create () in 286 + Emitter.emit emitter (Event.Stream_start { encoding = `Utf8 }); 287 + Emitter.emit emitter (Event.Document_start { version = None; implicit = true }); 288 + Emitter.emit emitter (Event.Mapping_start { anchor = None; tag = None; implicit = true; style = `Block }); 289 + (* Emit a key *) 290 + Emitter.emit emitter (Event.Scalar { anchor = None; tag = None; value = "url"; plain_implicit = true; quoted_implicit = true; style = `Plain }); 291 + (* Emit a folded scalar value *) 292 + Emitter.emit emitter (Event.Scalar { anchor = None; tag = None; value = "https://example.org/very/long/path"; plain_implicit = true; quoted_implicit = true; style = `Folded }); 293 + (* Emit another key-value pair *) 294 + Emitter.emit emitter (Event.Scalar { anchor = None; tag = None; value = "next"; plain_implicit = true; quoted_implicit = true; style = `Plain }); 295 + Emitter.emit emitter (Event.Scalar { anchor = None; tag = None; value = "value"; plain_implicit = true; quoted_implicit = true; style = `Plain }); 296 + Emitter.emit emitter Event.Mapping_end; 297 + Emitter.emit emitter (Event.Document_end { implicit = true }); 298 + Emitter.emit emitter Event.Stream_end; 299 + let result = Emitter.contents emitter in 300 + (* Check there's no double newline (blank line) in the output *) 301 + let has_double_newline = 302 + let rec check i = 303 + if i >= String.length result - 1 then false 304 + else if result.[i] = '\n' && result.[i+1] = '\n' then true 305 + else check (i + 1) 306 + in 307 + check 0 308 + in 309 + Alcotest.(check bool) "no double newlines in block scalar output" false has_double_newline; 310 + (* Also verify the output can be parsed back *) 311 + let parsed = of_string result in 312 + match parsed with 313 + | `O [ ("url", `String _); ("next", `String "value") ] -> () 314 + | _ -> Alcotest.fail ("expected mapping with url and next keys, got: " ^ result) 315 + 316 + let test_literal_block_no_double_newline () = 317 + let emitter = Emitter.create () in 318 + Emitter.emit emitter (Event.Stream_start { encoding = `Utf8 }); 319 + Emitter.emit emitter (Event.Document_start { version = None; implicit = true }); 320 + Emitter.emit emitter (Event.Mapping_start { anchor = None; tag = None; implicit = true; style = `Block }); 321 + Emitter.emit emitter (Event.Scalar { anchor = None; tag = None; value = "desc"; plain_implicit = true; quoted_implicit = true; style = `Plain }); 322 + Emitter.emit emitter (Event.Scalar { anchor = None; tag = None; value = "line1\nline2"; plain_implicit = true; quoted_implicit = true; style = `Literal }); 323 + Emitter.emit emitter (Event.Scalar { anchor = None; tag = None; value = "next"; plain_implicit = true; quoted_implicit = true; style = `Plain }); 324 + Emitter.emit emitter (Event.Scalar { anchor = None; tag = None; value = "value"; plain_implicit = true; quoted_implicit = true; style = `Plain }); 325 + Emitter.emit emitter Event.Mapping_end; 326 + Emitter.emit emitter (Event.Document_end { implicit = true }); 327 + Emitter.emit emitter Event.Stream_end; 328 + let result = Emitter.contents emitter in 329 + (* The output should be parseable and not have a blank line between the literal and next key *) 330 + let parsed = of_string result in 331 + match parsed with 332 + | `O [ ("desc", `String _); ("next", `String "value") ] -> () 333 + | _ -> Alcotest.fail ("expected mapping with desc and next keys, got: " ^ result) 334 + 278 335 let multiline_tests = 279 336 [ 280 337 ("literal block", `Quick, test_literal_block); 281 338 ("folded block", `Quick, test_folded_block); 339 + ("folded block no double newline", `Quick, test_block_scalar_no_double_newline); 340 + ("literal block no double newline", `Quick, test_literal_block_no_double_newline); 282 341 ] 283 342 284 343 (** Error handling tests *) ··· 289 348 Alcotest.fail "expected error" 290 349 with Yamlrw_error e -> Alcotest.(check bool) "has span" true (e.span <> None) 291 350 292 - let error_tests = [ ("error position", `Quick, test_error_position) ] 351 + let test_invalid_unicode_escape () = 352 + (* Unicode scalar values must be 0x0000-0x10FFFF, excluding surrogates *) 353 + (* Test \U with value > 0x10FFFF (maximum valid Unicode codepoint) *) 354 + (try 355 + let _ = of_string "\"\\U88888888\"" in 356 + Alcotest.fail "expected Invalid_unicode_escape error for out-of-range" 357 + with Yamlrw_error e -> ( 358 + match e.Error.kind with 359 + | Error.Invalid_unicode_escape _ -> () 360 + | _ -> 361 + Alcotest.fail 362 + ("expected Invalid_unicode_escape error, got: " 363 + ^ Error.kind_to_string e.Error.kind))); 364 + (* Test \u with surrogate codepoint (should error) *) 365 + (try 366 + let _ = of_string "\"\\uD800\"" in 367 + Alcotest.fail "expected Invalid_unicode_escape error for surrogate" 368 + with Yamlrw_error e -> ( 369 + match e.Error.kind with 370 + | Error.Invalid_unicode_escape _ -> () 371 + | _ -> 372 + Alcotest.fail 373 + ("expected Invalid_unicode_escape error, got: " 374 + ^ Error.kind_to_string e.Error.kind))); 375 + (* Test \u with valid value (should work) *) 376 + let v = of_string "\"\\u0041\"" in 377 + Alcotest.(check string) "valid \\u escape" "A" (Value.to_string v); 378 + (* Test \U with valid value at max boundary (should work) *) 379 + let v2 = of_string "\"\\U0010FFFF\"" in 380 + Alcotest.(check bool) 381 + "valid \\U at max boundary" true 382 + (String.length (Value.to_string v2) > 0) 383 + 384 + let error_tests = 385 + [ 386 + ("error position", `Quick, test_error_position); 387 + ("invalid unicode escape", `Quick, test_invalid_unicode_escape); 388 + ] 293 389 294 390 (** Alias expansion limit tests (billion laughs protection) *) 295 391
+1
ocaml-yamlrw/yamlrw.opam
··· 17 17 "mdx" {with-doc} 18 18 "jsonm" {with-test} 19 19 "alcotest" {with-test} 20 + "crowbar" {with-test} 20 21 ] 21 22 build: [ 22 23 ["dune" "subst"] {dev}