My agentic slop goes here. Not intended for anyone else!

sync

Changed files
+451 -120
yaml
+1 -1
yaml/ocaml-yamle/bin/dune
···
(executable
(name yamlcat)
(public_name yamlcat)
-
(libraries yamle))
+
(libraries yamle cmdliner))
(executable
(name test_emit)
+92 -47
yaml/ocaml-yamle/bin/yamlcat.ml
···
(** yamlcat - parse and reprint YAML files *)
-
let usage () =
-
Printf.eprintf "Usage: %s [OPTIONS] [FILE...]\n" Sys.argv.(0);
-
Printf.eprintf "\n";
-
Printf.eprintf "Parse YAML files and reprint them.\n";
-
Printf.eprintf "If no files are given, reads from stdin.\n";
-
Printf.eprintf "\n";
-
Printf.eprintf "Options:\n";
-
Printf.eprintf " --all Output all documents (for multi-document YAML)\n";
-
Printf.eprintf " --json Output as JSON format\n";
-
Printf.eprintf " --flow Output YAML in flow style\n";
-
Printf.eprintf " --debug Output internal representation (for debugging)\n";
-
Printf.eprintf " --help Show this help message\n";
-
exit 1
+
open Cmdliner
type output_format = Yaml | Json | Flow | Debug
···
json_to_string buf v;
Buffer.contents buf
-
let process_string ~format ~all content =
+
let process_string ~format ~all ~resolve_aliases ~max_nodes ~max_depth content =
try
if all then
(* Multi-document mode *)
···
| Some yaml ->
if not !first then print_endline "---";
first := false;
-
let value = Yamle.to_json yaml in
+
let value = Yamle.to_json ~resolve_aliases ~max_nodes ~max_depth yaml in
print_endline (value_to_json value)
) documents
| Debug ->
···
(* Single-document mode (original behavior) *)
match format with
| Yaml ->
-
let value = Yamle.of_string content in
+
let value = Yamle.of_string ~resolve_aliases ~max_nodes ~max_depth content in
print_string (Yamle.to_string value)
| Flow ->
-
let value = Yamle.of_string content in
+
let value = Yamle.of_string ~resolve_aliases ~max_nodes ~max_depth content in
print_string (Yamle.to_string ~layout_style:Yamle.Layout_style.Flow value)
| Json ->
-
let value = Yamle.of_string content in
+
let value = Yamle.of_string ~resolve_aliases ~max_nodes ~max_depth content in
print_endline (value_to_json value)
| Debug ->
-
let yaml = Yamle.yaml_of_string content in
+
let yaml = Yamle.yaml_of_string ~resolve_aliases ~max_nodes ~max_depth content in
Format.printf "%a@." Yamle.pp_yaml yaml
with
| Yamle.Yamle_error e ->
Printf.eprintf "Error: %s\n" (Yamle.Error.to_string e);
exit 1
-
let process_file ~format ~all filename =
+
let process_file ~format ~all ~resolve_aliases ~max_nodes ~max_depth filename =
let content =
if filename = "-" then
In_channel.input_all In_channel.stdin
else
In_channel.with_open_text filename In_channel.input_all
in
-
process_string ~format ~all content
+
process_string ~format ~all ~resolve_aliases ~max_nodes ~max_depth content
+
+
let run format all resolve_aliases max_nodes max_depth files =
+
let files = if files = [] then ["-"] else files in
+
List.iter (process_file ~format ~all ~resolve_aliases ~max_nodes ~max_depth) files;
+
`Ok ()
+
+
(* Command-line arguments *)
+
+
let format_arg =
+
let doc = "Output format: yaml (default), json, flow, or debug." in
+
let formats = [
+
("yaml", Yaml);
+
("json", Json);
+
("flow", Flow);
+
("debug", Debug);
+
] in
+
Arg.(value & opt (enum formats) Yaml & info ["format"; "f"] ~docv:"FORMAT" ~doc)
+
+
let json_arg =
+
let doc = "Output as JSON (shorthand for --format=json)." in
+
Arg.(value & flag & info ["json"] ~doc)
+
+
let flow_arg =
+
let doc = "Output in flow style (shorthand for --format=flow)." in
+
Arg.(value & flag & info ["flow"] ~doc)
+
+
let debug_arg =
+
let doc = "Output internal representation (shorthand for --format=debug)." in
+
Arg.(value & flag & info ["debug"] ~doc)
+
+
let all_arg =
+
let doc = "Output all documents (for multi-document YAML)." in
+
Arg.(value & flag & info ["all"; "a"] ~doc)
+
+
let no_resolve_aliases_arg =
+
let doc = "Don't resolve aliases (keep them as references)." in
+
Arg.(value & flag & info ["no-resolve-aliases"] ~doc)
+
+
let max_nodes_arg =
+
let doc = "Maximum number of nodes during alias expansion (default: 10000000). \
+
Protection against billion laughs attack." in
+
Arg.(value & opt int Yamle.default_max_alias_nodes & info ["max-nodes"] ~docv:"N" ~doc)
-
let () =
-
let files = ref [] in
-
let format = ref Yaml in
-
let show_help = ref false in
-
let all = ref false in
+
let max_depth_arg =
+
let doc = "Maximum alias nesting depth (default: 100). \
+
Protection against deeply nested alias chains." in
+
Arg.(value & opt int Yamle.default_max_alias_depth & info ["max-depth"] ~docv:"N" ~doc)
-
(* Parse arguments *)
-
let args = Array.to_list Sys.argv |> List.tl in
-
List.iter (fun arg ->
-
match arg with
-
| "--help" | "-h" -> show_help := true
-
| "--all" -> all := true
-
| "--json" -> format := Json
-
| "--flow" -> format := Flow
-
| "--debug" -> format := Debug
-
| s when String.length s > 0 && s.[0] = '-' ->
-
Printf.eprintf "Unknown option: %s\n" s;
-
usage ()
-
| filename -> files := filename :: !files
-
) args;
+
let files_arg =
+
let doc = "YAML file(s) to process. Use '-' for stdin." in
+
Arg.(value & pos_all file [] & info [] ~docv:"FILE" ~doc)
+
+
let combined_format format json flow debug =
+
if json then Json
+
else if flow then Flow
+
else if debug then Debug
+
else format
-
if !show_help then usage ();
+
let term =
+
let combine format json flow debug all no_resolve max_nodes max_depth files =
+
let format = combined_format format json flow debug in
+
let resolve_aliases = not no_resolve in
+
run format all resolve_aliases max_nodes max_depth files
+
in
+
Term.(ret (const combine $ format_arg $ json_arg $ flow_arg $ debug_arg $
+
all_arg $ no_resolve_aliases_arg $ max_nodes_arg $ max_depth_arg $ files_arg))
-
let files = List.rev !files in
+
let info =
+
let doc = "Parse and reprint YAML files" in
+
let man = [
+
`S Manpage.s_description;
+
`P "$(tname) parses YAML files and reprints them in various formats. \
+
It can be used to validate YAML, convert between styles, or convert to JSON.";
+
`S Manpage.s_examples;
+
`P "Parse and reprint a YAML file:";
+
`Pre " $(tname) config.yaml";
+
`P "Convert YAML to JSON:";
+
`Pre " $(tname) --json config.yaml";
+
`P "Process multi-document YAML:";
+
`Pre " $(tname) --all multi.yaml";
+
`P "Limit alias expansion (protection against malicious YAML):";
+
`Pre " $(tname) --max-nodes 1000 --max-depth 10 untrusted.yaml";
+
`S Manpage.s_bugs;
+
`P "Report bugs at https://github.com/avsm/ocaml-yaml/issues";
+
] in
+
Cmd.info "yamlcat" ~version:"0.1.0" ~doc ~man
-
if files = [] then
-
(* Read from stdin *)
-
process_file ~format:!format ~all:!all "-"
-
else
-
List.iter (process_file ~format:!format ~all:!all) files
+
let () = exit (Cmd.eval (Cmd.v info term))
+6
yaml/ocaml-yamle/lib/error.ml
···
| Type_mismatch of string * string (** expected, got *)
| Unresolved_alias of string
| Key_not_found of string
+
| Alias_expansion_node_limit of int (** max nodes exceeded *)
+
| Alias_expansion_depth_limit of int (** max depth exceeded *)
(* Emitter errors *)
| Invalid_encoding of string
···
Printf.sprintf "type mismatch: expected %s, got %s" expected got
| Unresolved_alias s -> Printf.sprintf "unresolved alias: *%s" s
| Key_not_found s -> Printf.sprintf "key not found: %s" s
+
| Alias_expansion_node_limit n ->
+
Printf.sprintf "alias expansion exceeded node limit (%d nodes)" n
+
| Alias_expansion_depth_limit n ->
+
Printf.sprintf "alias expansion exceeded depth limit (%d levels)" n
| Invalid_encoding s -> Printf.sprintf "invalid encoding: %s" s
| Scalar_contains_invalid_chars s ->
Printf.sprintf "scalar contains invalid characters: %s" s
+40 -11
yaml/ocaml-yamle/lib/loader.ml
···
pending_key = None;
} :: rest)
-
(** Load single document as Value *)
-
let value_of_string s =
+
(** Load single document as Value.
+
+
@param resolve_aliases Whether to resolve aliases (default true)
+
@param max_nodes Maximum nodes during alias expansion (default 10M)
+
@param max_depth Maximum alias nesting depth (default 100)
+
*)
+
let value_of_string
+
?(resolve_aliases = true)
+
?(max_nodes = Yaml.default_max_alias_nodes)
+
?(max_depth = Yaml.default_max_alias_depth)
+
s =
let parser = Parser.of_string s in
let state = create_state () in
Parser.iter (process_event state) parser;
···
(match Document.root doc with
| None -> `Null
| Some yaml ->
-
let yaml = Yaml.resolve_aliases yaml in
-
Yaml.to_value yaml)
+
Yaml.to_value ~resolve_aliases_first:resolve_aliases ~max_nodes ~max_depth yaml)
| _ -> Error.raise Multiple_documents
-
(** Load single document as Yaml *)
-
let yaml_of_string s =
+
(** Load single document as Yaml.
+
+
@param resolve_aliases Whether to resolve aliases (default false for Yaml.t)
+
@param max_nodes Maximum nodes during alias expansion (default 10M)
+
@param max_depth Maximum alias nesting depth (default 100)
+
*)
+
let yaml_of_string
+
?(resolve_aliases = false)
+
?(max_nodes = Yaml.default_max_alias_nodes)
+
?(max_depth = Yaml.default_max_alias_depth)
+
s =
let parser = Parser.of_string s in
let state = create_state () in
Parser.iter (process_event state) parser;
···
| [doc] ->
(match Document.root doc with
| None -> `Scalar (Scalar.make "")
-
| Some yaml -> yaml)
+
| Some yaml ->
+
if resolve_aliases then
+
Yaml.resolve_aliases ~max_nodes ~max_depth yaml
+
else
+
yaml)
| _ -> Error.raise Multiple_documents
(** Load all documents *)
···
Parser.iter (process_event state) parser;
List.rev state.documents
-
(** Load single Value from parser *)
-
let load_value parser =
+
(** Load single Value from parser.
+
+
@param resolve_aliases Whether to resolve aliases (default true)
+
@param max_nodes Maximum nodes during alias expansion (default 10M)
+
@param max_depth Maximum alias nesting depth (default 100)
+
*)
+
let load_value
+
?(resolve_aliases = true)
+
?(max_nodes = Yaml.default_max_alias_nodes)
+
?(max_depth = Yaml.default_max_alias_depth)
+
parser =
let state = create_state () in
let rec loop () =
match Parser.next parser with
···
Some (match Document.root doc with
| None -> `Null
| Some yaml ->
-
let yaml = Yaml.resolve_aliases yaml in
-
Yaml.to_value yaml)
+
Yaml.to_value ~resolve_aliases_first:resolve_aliases ~max_nodes ~max_depth yaml)
| [] -> None)
| Event.Stream_end -> None
| _ -> loop ()
+89 -56
yaml/ocaml-yamle/lib/yaml.ml
···
(`Scalar (Scalar.make k), of_value v)
) pairs))
-
(** Convert to JSON-compatible Value *)
+
(** Default limits for alias expansion (protection against billion laughs attack) *)
+
let default_max_alias_nodes = 10_000_000
+
let default_max_alias_depth = 100
+
+
(** Resolve aliases by replacing them with referenced nodes.
+
+
@param max_nodes Maximum number of nodes to create during expansion (default 10M)
+
@param max_depth Maximum depth of alias-within-alias resolution (default 100)
+
@raise Alias_expansion_node_limit if max_nodes is exceeded
+
@raise Alias_expansion_depth_limit if max_depth is exceeded
+
*)
+
let resolve_aliases ?(max_nodes = default_max_alias_nodes) ?(max_depth = default_max_alias_depth) (root : t) : t =
+
let anchors = Hashtbl.create 16 in
+
let node_count = ref 0 in
+
+
(* Check node limit *)
+
let check_node_limit () =
+
incr node_count;
+
if !node_count > max_nodes then
+
Error.raise (Alias_expansion_node_limit max_nodes)
+
in
+
+
(* First pass: collect all anchors *)
+
let rec collect (v : t) =
+
match v with
+
| `Scalar s ->
+
(match Scalar.anchor s with
+
| Some name -> Hashtbl.replace anchors name v
+
| None -> ())
+
| `Alias _ -> ()
+
| `A seq ->
+
(match Sequence.anchor seq with
+
| Some name -> Hashtbl.replace anchors name v
+
| None -> ());
+
List.iter collect (Sequence.members seq)
+
| `O map ->
+
(match Mapping.anchor map with
+
| Some name -> Hashtbl.replace anchors name v
+
| None -> ());
+
List.iter (fun (k, v) -> collect k; collect v) (Mapping.members map)
+
in
+
collect root;
-
let rec to_value (v : t) : Value.t =
-
match v with
-
| `Scalar s -> scalar_to_value s
-
| `Alias name -> Error.raise (Unresolved_alias name)
-
| `A seq -> `A (List.map to_value (Sequence.members seq))
-
| `O map ->
-
`O (List.map (fun (k, v) ->
-
let key = match k with
-
| `Scalar s -> Scalar.value s
-
| _ -> Error.raise (Type_mismatch ("string key", "complex key"))
-
in
-
(key, to_value v)
-
) (Mapping.members map))
+
(* Second pass: resolve aliases with depth tracking *)
+
let rec resolve ~depth (v : t) : t =
+
check_node_limit ();
+
match v with
+
| `Scalar _ -> v
+
| `Alias name ->
+
if depth >= max_depth then
+
Error.raise (Alias_expansion_depth_limit max_depth);
+
(match Hashtbl.find_opt anchors name with
+
| Some target -> resolve ~depth:(depth + 1) target
+
| None -> Error.raise (Undefined_alias name))
+
| `A seq ->
+
`A (Sequence.map (resolve ~depth) seq)
+
| `O map ->
+
`O (Mapping.make
+
?anchor:(Mapping.anchor map)
+
?tag:(Mapping.tag map)
+
~implicit:(Mapping.implicit map)
+
~style:(Mapping.style map)
+
(List.map (fun (k, v) -> (resolve ~depth k, resolve ~depth v)) (Mapping.members map)))
+
in
+
resolve ~depth:0 root
(** Convert scalar to JSON value based on content *)
-
and scalar_to_value s =
+
let rec scalar_to_value s =
let value = Scalar.value s in
let tag = Scalar.tag s in
let style = Scalar.style s in
···
(* Not a number - it's a string *)
`String value
-
(** Resolve aliases by replacing them with referenced nodes *)
+
(** Convert to JSON-compatible Value.
-
let resolve_aliases (root : t) : t =
-
let anchors = Hashtbl.create 16 in
-
-
(* First pass: collect all anchors *)
-
let rec collect (v : t) =
+
@param resolve_aliases_first Whether to resolve aliases before conversion (default true)
+
@param max_nodes Maximum nodes during alias expansion (default 10M)
+
@param max_depth Maximum alias nesting depth (default 100)
+
@raise Unresolved_alias if resolve_aliases_first is false and an alias is encountered
+
*)
+
let to_value
+
?(resolve_aliases_first = true)
+
?(max_nodes = default_max_alias_nodes)
+
?(max_depth = default_max_alias_depth)
+
(v : t) : Value.t =
+
let v = if resolve_aliases_first then resolve_aliases ~max_nodes ~max_depth v else v in
+
let rec convert (v : t) : Value.t =
match v with
-
| `Scalar s ->
-
(match Scalar.anchor s with
-
| Some name -> Hashtbl.replace anchors name v
-
| None -> ())
-
| `Alias _ -> ()
-
| `A seq ->
-
(match Sequence.anchor seq with
-
| Some name -> Hashtbl.replace anchors name v
-
| None -> ());
-
List.iter collect (Sequence.members seq)
+
| `Scalar s -> scalar_to_value s
+
| `Alias name -> Error.raise (Unresolved_alias name)
+
| `A seq -> `A (List.map convert (Sequence.members seq))
| `O map ->
-
(match Mapping.anchor map with
-
| Some name -> Hashtbl.replace anchors name v
-
| None -> ());
-
List.iter (fun (k, v) -> collect k; collect v) (Mapping.members map)
-
in
-
collect root;
-
-
(* Second pass: resolve aliases *)
-
let rec resolve (v : t) : t =
-
match v with
-
| `Scalar _ -> v
-
| `Alias name ->
-
(match Hashtbl.find_opt anchors name with
-
| Some target -> resolve target
-
| None -> Error.raise (Undefined_alias name))
-
| `A seq ->
-
`A (Sequence.map resolve seq)
-
| `O map ->
-
`O (Mapping.make
-
?anchor:(Mapping.anchor map)
-
?tag:(Mapping.tag map)
-
~implicit:(Mapping.implicit map)
-
~style:(Mapping.style map)
-
(List.map (fun (k, v) -> (resolve k, resolve v)) (Mapping.members map)))
+
`O (List.map (fun (k, v) ->
+
let key = match k with
+
| `Scalar s -> Scalar.value s
+
| _ -> Error.raise (Type_mismatch ("string key", "complex key"))
+
in
+
(key, convert v)
+
) (Mapping.members map))
in
-
resolve root
+
convert v
(** Get anchor from any node *)
let anchor (v : t) =
+23 -5
yaml/ocaml-yamle/lib/yamle.ml
···
type error = Error.t
exception Yamle_error = Error.Yamle_error
+
(** {1 Alias expansion limits (protection against billion laughs attack)} *)
+
+
let default_max_alias_nodes = Yaml.default_max_alias_nodes
+
let default_max_alias_depth = Yaml.default_max_alias_depth
+
(** {1 JSON-compatible parsing} *)
-
let of_string s = Loader.value_of_string s
+
let of_string
+
?(resolve_aliases = true)
+
?(max_nodes = default_max_alias_nodes)
+
?(max_depth = default_max_alias_depth)
+
s =
+
Loader.value_of_string ~resolve_aliases ~max_nodes ~max_depth s
let documents_of_string s = Loader.documents_of_string s
···
(** {1 YAML-specific parsing} *)
-
let yaml_of_string s = Loader.yaml_of_string s
+
let yaml_of_string
+
?(resolve_aliases = false)
+
?(max_nodes = default_max_alias_nodes)
+
?(max_depth = default_max_alias_depth)
+
s =
+
Loader.yaml_of_string ~resolve_aliases ~max_nodes ~max_depth s
(** {1 YAML-specific emission} *)
···
(** {1 Conversion} *)
-
let to_json yaml =
-
let yaml = Yaml.resolve_aliases yaml in
-
Yaml.to_value yaml
+
let to_json
+
?(resolve_aliases = true)
+
?(max_nodes = default_max_alias_nodes)
+
?(max_depth = default_max_alias_depth)
+
yaml =
+
Yaml.to_value ~resolve_aliases_first:resolve_aliases ~max_nodes ~max_depth yaml
let of_json value = Yaml.of_value value
+113
yaml/ocaml-yamle/tests/cram/bomb.t
···
+
Billion laughs attack protection tests
+
+
Create a small bomb file for testing:
+
+
$ cat > bomb_small.yml << 'EOF'
+
> # Small "billion laughs" style YAML bomb for testing
+
> # Expands to 9^4 = 6561 nodes when aliases are resolved
+
> a: &a [1, 2, 3, 4, 5, 6, 7, 8, 9]
+
> b: &b [*a, *a, *a, *a, *a, *a, *a, *a, *a]
+
> c: &c [*b, *b, *b, *b, *b, *b, *b, *b, *b]
+
> d: &d [*c, *c, *c, *c, *c, *c, *c, *c, *c]
+
> EOF
+
+
Test with a tight node limit (small bomb would expand to ~6561 nodes):
+
+
$ yamlcat --max-nodes 100 --json bomb_small.yml
+
Error: alias expansion exceeded node limit (100 nodes)
+
[1]
+
+
Test with a limit that allows the small bomb:
+
+
$ yamlcat --max-nodes 10000 --json bomb_small.yml | head -c 100
+
{"a": [1, 2, 3, 4, 5, 6, 7, 8, 9], "b": [[1, 2, 3, 4, 5, 6, 7, 8, 9], [1, 2, 3, 4, 5, 6, 7, 8, 9], [
+
+
Test depth limit with a nested alias chain:
+
+
$ cat > depth_bomb.yml << 'EOF'
+
> a: &a [x, y, z]
+
> b: &b [*a, *a]
+
> c: &c [*b, *b]
+
> d: &d [*c, *c]
+
> e: &e [*d, *d]
+
> result: *e
+
> EOF
+
+
$ yamlcat --max-depth 2 --json depth_bomb.yml
+
Error: alias expansion exceeded depth limit (2 levels)
+
[1]
+
+
$ yamlcat --max-depth 10 --json depth_bomb.yml | head -c 50
+
{"a": ["x", "y", "z"], "b": [["x", "y", "z"], ["x"
+
+
Test that --no-resolve-aliases keeps aliases as-is (in debug mode):
+
+
$ cat > simple_alias.yml << 'EOF'
+
> anchor: &anc hello
+
> alias: *anc
+
> EOF
+
+
$ yamlcat --no-resolve-aliases --debug simple_alias.yml
+
mapping(
+
style=block,
+
members={
+
scalar("anchor", style=plain): scalar("hello", anchor=anc, style=plain),
+
scalar("alias", style=plain): *anc
+
})
+
+
With resolve (default), aliases are expanded:
+
+
$ yamlcat --json simple_alias.yml
+
{"anchor": "hello", "alias": "hello"}
+
+
Create a full bomb (like the one in ocaml-yaml):
+
+
$ cat > bomb.yml << 'EOF'
+
> a: &a ["lol","lol","lol","lol","lol","lol","lol","lol","lol"]
+
> b: &b [*a,*a,*a,*a,*a,*a,*a,*a,*a]
+
> c: &c [*b,*b,*b,*b,*b,*b,*b,*b,*b]
+
> d: &d [*c,*c,*c,*c,*c,*c,*c,*c,*c]
+
> e: &e [*d,*d,*d,*d,*d,*d,*d,*d,*d]
+
> f: &f [*e,*e,*e,*e,*e,*e,*e,*e,*e]
+
> g: &g [*f,*f,*f,*f,*f,*f,*f,*f,*f]
+
> h: &h [*g,*g,*g,*g,*g,*g,*g,*g,*g]
+
> i: &i [*h,*h,*h,*h,*h,*h,*h,*h,*h]
+
> EOF
+
+
Test the full bomb is rejected with default limits:
+
+
$ yamlcat --json bomb.yml 2>&1 | head -1
+
Error: alias expansion exceeded node limit (10000000 nodes)
+
+
With a very small limit:
+
+
$ yamlcat --max-nodes 50 --json bomb.yml
+
Error: alias expansion exceeded node limit (50 nodes)
+
[1]
+
+
Test that valid YAML with aliases works:
+
+
$ cat > valid.yml << 'EOF'
+
> defaults: &defaults
+
> timeout: 30
+
> retries: 3
+
> production:
+
> <<: *defaults
+
> port: 8080
+
> EOF
+
+
$ yamlcat --json valid.yml
+
{"defaults": {"timeout": 30, "retries": 3}, "production": {"<<": {"timeout": 30, "retries": 3}, "port": 8080}}
+
+
Test help includes the new options:
+
+
$ yamlcat --help=plain | grep 'max-nodes'
+
--max-nodes=N (absent=10000000)
+
yamlcat --max-nodes 1000 --max-depth 10 untrusted.yaml
+
+
$ yamlcat --help=plain | grep 'max-depth'
+
--max-depth=N (absent=100)
+
yamlcat --max-nodes 1000 --max-depth 10 untrusted.yaml
+
+
$ yamlcat --help=plain | grep 'no-resolve-aliases'
+
--no-resolve-aliases
+6
yaml/ocaml-yamle/tests/cram/bomb_small.yml
···
+
# Small "billion laughs" style YAML bomb for testing
+
# Expands to 9^4 = 6561 nodes when aliases are resolved
+
a: &a [1, 2, 3, 4, 5, 6, 7, 8, 9]
+
b: &b [*a, *a, *a, *a, *a, *a, *a, *a, *a]
+
c: &c [*b, *b, *b, *b, *b, *b, *b, *b, *b]
+
d: &d [*c, *c, *c, *c, *c, *c, *c, *c, *c]
+81
yaml/ocaml-yamle/tests/test_yamle.ml
···
"error position", `Quick, test_error_position;
]
+
(** Alias expansion limit tests (billion laughs protection) *)
+
+
let test_node_limit () =
+
(* Small bomb that would expand to 9^4 = 6561 nodes *)
+
let yaml = {|
+
a: &a [1,2,3,4,5,6,7,8,9]
+
b: &b [*a,*a,*a,*a,*a,*a,*a,*a,*a]
+
c: &c [*b,*b,*b,*b,*b,*b,*b,*b,*b]
+
d: &d [*c,*c,*c,*c,*c,*c,*c,*c,*c]
+
|} in
+
(* Should fail with a small node limit *)
+
try
+
let _ = of_string ~max_nodes:100 yaml in
+
Alcotest.fail "expected node limit error"
+
with
+
| Yamle_error e ->
+
(match e.Error.kind with
+
| Error.Alias_expansion_node_limit _ -> ()
+
| _ -> Alcotest.fail "expected Alias_expansion_node_limit error")
+
+
let test_depth_limit () =
+
(* Create deeply nested alias chain:
+
*e -> [*d,*d] -> [*c,*c] -> [*b,*b] -> [*a,*a] -> [x,y,z]
+
Each alias resolution increases depth by 1 *)
+
let yaml = {|
+
a: &a [x, y, z]
+
b: &b [*a, *a]
+
c: &c [*b, *b]
+
d: &d [*c, *c]
+
e: &e [*d, *d]
+
result: *e
+
|} in
+
(* Should fail with a small depth limit (depth 3 means max 3 alias hops) *)
+
try
+
let _ = of_string ~max_depth:3 yaml in
+
Alcotest.fail "expected depth limit error"
+
with
+
| Yamle_error e ->
+
(match e.Error.kind with
+
| Error.Alias_expansion_depth_limit _ -> ()
+
| _ -> Alcotest.fail ("expected Alias_expansion_depth_limit error, got: " ^ Error.kind_to_string e.Error.kind))
+
+
let test_normal_aliases_work () =
+
(* Normal alias usage should work fine *)
+
let yaml = {|
+
defaults: &defaults
+
timeout: 30
+
retries: 3
+
production:
+
<<: *defaults
+
port: 8080
+
|} in
+
let result = of_string yaml in
+
match result with
+
| `O _ -> ()
+
| _ -> Alcotest.fail "expected mapping"
+
+
let test_resolve_aliases_false () =
+
(* With resolve_aliases=false, aliases should remain unresolved *)
+
let yaml = {|
+
a: &anchor value
+
b: *anchor
+
|} in
+
let result = yaml_of_string ~resolve_aliases:false yaml in
+
(* Check that alias is preserved *)
+
match result with
+
| `O map ->
+
let pairs = Mapping.members map in
+
(match List.assoc_opt (`Scalar (Scalar.make "b")) pairs with
+
| Some (`Alias "anchor") -> ()
+
| _ -> Alcotest.fail "expected alias to be preserved")
+
| _ -> Alcotest.fail "expected mapping"
+
+
let alias_limit_tests = [
+
"node limit", `Quick, test_node_limit;
+
"depth limit", `Quick, test_depth_limit;
+
"normal aliases work", `Quick, test_normal_aliases_work;
+
"resolve_aliases false", `Quick, test_resolve_aliases_false;
+
]
+
(** Run all tests *)
let () =
···
"yaml", yaml_tests;
"multiline", multiline_tests;
"errors", error_tests;
+
"alias_limits", alias_limit_tests;
]