diff --git a/Cargo.lock b/Cargo.lock index 269c2418..66984c6b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -291,7 +291,6 @@ dependencies = [ "rustyline", "serde", "serde_json", - "serde_yaml", "sha2", "signal-hook", "tempfile", @@ -301,6 +300,7 @@ dependencies = [ "unicode-width", "url", "uuid", + "yaml_serde", ] [[package]] @@ -842,6 +842,12 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "libyaml-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e126dda6f34391ab7b444f9922055facc83c07a910da3eb16f1e4d9c45dc777" + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -1409,19 +1415,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" -dependencies = [ - "indexmap", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - [[package]] name = "sha2" version = "0.10.9" @@ -1783,12 +1776,6 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" -[[package]] -name = "unsafe-libyaml" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" - [[package]] name = "untrusted" version = "0.9.0" @@ -2156,6 +2143,19 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +[[package]] +name = "yaml_serde" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b729a08a9a6be689bbad3e2bf8015926db54b6622cc89c3a5f7dc174b9e918" +dependencies = [ + "indexmap", + "itoa", + "libyaml-rs", + "ryu", + "serde", +] + [[package]] name = "yoke" version = "0.8.3" diff --git a/Cargo.toml b/Cargo.toml index 80157253..d4266b9d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,7 +40,7 @@ reqwest = { version = "0.12", features = ["blocking", "rustls-tls", "json"], def rustyline = { version = "14", default-features = false, features = ["custom-bindings", "with-file-history"] } serde = { version = "1", features = ["derive"] } serde_json = { version = "1", features = ["raw_value"] } -serde_yaml = "0.9" +serde_yaml = { package = "yaml_serde", version = "0.10.7" } sha2 = "0.10" signal-hook = "0.3" tempfile = "3" diff --git a/deny.toml b/deny.toml index 3439831d..7b96ae71 100644 --- a/deny.toml +++ b/deny.toml @@ -11,13 +11,7 @@ [advisories] # A new advisory must fail the check; add an entry here only with a written # reason and a link to the tracking Issue. -ignore = [ - # unsafe-libyaml is unmaintained (published 2026-10). It reaches us only - # through serde_yaml 0.9, which has no safe upgrade. Temporary, approved by - # the user 2026-10-13: remove this entry in the PR that migrates off - # serde_yaml (https://github.com/Kewton/CommandAgent/issues/530). - { id = "RUSTSEC-2024-0449", reason = "unsafe-libyaml via serde_yaml 0.9; migration tracked in #530" }, -] +ignore = [] [licenses] # Curated allow list matching the current dependency graph. BSL-1.0 is required diff --git a/tests/corpus/apps/issue530-yaml-migration/expectations.toml b/tests/corpus/apps/issue530-yaml-migration/expectations.toml new file mode 100644 index 00000000..df92988e --- /dev/null +++ b/tests/corpus/apps/issue530-yaml-migration/expectations.toml @@ -0,0 +1,31 @@ +case_id = "issue530-yaml-migration" +source = "Issue #530: serde_yaml 0.9 (unsafe-libyaml, RUSTSEC-2024-0449) migrated to yaml_serde 0.10.7 (libyaml-rs); pre-migration measurements are recorded in the fixture and reproduced through the public API" +required_paths = [] +verify_commands = [] +required_capabilities = [] +required_evidence = [] +required_obligations = [] +deferred_verify_requirements = [] +evidence_hint_tokens = [] + +[route_closure] +include = [] +exclude = [] + +[weak_evidence] +contains = [] + +[diagnostics] +contains = [] + +[compile] +expect = "not_checked" + +[fixture_contains] +fixtures/compatibility.json = [ + "\"migrated_from\": \"serde_yaml 0.9.34+deprecated\"", + "\"migrated_to\": \"yaml_serde 0.10.7\"", + "\"id\": \"anchor-alias-rejected\"", + "\"schema_accepts\": false", + "\"parser_accepts\": true" +] diff --git a/tests/corpus/apps/issue530-yaml-migration/fixtures/compatibility.json b/tests/corpus/apps/issue530-yaml-migration/fixtures/compatibility.json new file mode 100644 index 00000000..a7247a2a --- /dev/null +++ b/tests/corpus/apps/issue530-yaml-migration/fixtures/compatibility.json @@ -0,0 +1,219 @@ +{ + "issue": 530, + "migrated_from": "serde_yaml 0.9.34+deprecated", + "migrated_to": "yaml_serde 0.10.7", + "parser_dependency_from": "unsafe-libyaml 0.2.11 (RUSTSEC-2024-0449, unmaintained)", + "parser_dependency_to": "libyaml-rs 0.3.0", + "baseline": "Measured on the pre-migration develop export; yaml_serde 0.10.7 reproduces accept/reject, type, value, tag, arbitrary-key, serialization, and diagnostic line/column identically.", + "pack_cases": [ + { + "id": "valid-assist-accepted", + "document": "schema_version: commandagent.pack.assist/v0\npack:\n id: ingest-default\n version: 1.0.0\n profile: ingest\n intent: create\ninject:\n - point: declare-ingest-inspection\n source: ingest_snapshot_structure_injected\n required: true\n params:\n max_files: 8\nliterals:\n - gate: ingest_source_binding\n example:\n format: json\n value: '{\"date\":\"2026-08-03\"}'\nvocabulary:\n - point: implement-ingest-delivery\n source: ingest_candidate_ids_injected\n mode: verbatim\n required: true\n params: {}\n", + "parser_accepts": true, + "schema_accepts": true + }, + { + "id": "unknown-field-rejected", + "document": "schema_version: commandagent.pack.assist/v0\npack:\n id: ingest-default\n version: 1.0.0\n profile: ingest\n intent: create\ninvented: true\ninject:\n - point: declare-ingest-inspection\n source: ingest_snapshot_structure_injected\n required: true\n params:\n max_files: 8\nliterals:\n - gate: ingest_source_binding\n example:\n format: json\n value: '{\"date\":\"2026-08-03\"}'\nvocabulary:\n - point: implement-ingest-delivery\n source: ingest_candidate_ids_injected\n mode: verbatim\n required: true\n params: {}\n", + "parser_accepts": true, + "schema_accepts": false, + "schema_error": "unknown field `invented`, expected one of `schema_version`, `pack`, `inject`, `literals`, `vocabulary` at line 7 column 1" + }, + { + "id": "duplicate-key-rejected", + "document": "schema_version: commandagent.pack.assist/v0\npack:\n id: ingest-default\n id: duplicate\n version: 1.0.0\n profile: ingest\n intent: create\ninject:\n - point: declare-ingest-inspection\n source: ingest_snapshot_structure_injected\n required: true\n params:\n max_files: 8\nliterals:\n - gate: ingest_source_binding\n example:\n format: json\n value: '{\"date\":\"2026-08-03\"}'\nvocabulary:\n - point: implement-ingest-delivery\n source: ingest_candidate_ids_injected\n mode: verbatim\n required: true\n params: {}\n", + "parser_accepts": false, + "schema_accepts": false, + "schema_error": "duplicate YAML key `id` at line 3 column 3" + }, + { + "id": "anchor-alias-rejected", + "document": "schema_version: commandagent.pack.assist/v0\npack:\n id: ingest-default\n version: 1.0.0\n profile: ingest\n intent: create\ninject:\n - point: declare-ingest-inspection\n source: ingest_snapshot_structure_injected\n required: true\n params:\n max_files: 8\nliterals:\n - gate: ingest_source_binding\n example:\n format: json\n value: '{\"date\":\"2026-08-03\"}'\nvocabulary:\n - point: implement-ingest-delivery\n source: ingest_candidate_ids_injected\n mode: verbatim\n required: true\n params: {}\nnote: &n hello\nref: *n\n", + "parser_accepts": true, + "schema_accepts": false, + "schema_error": "YAML anchors are forbidden at line 24" + }, + { + "id": "tag-rejected", + "document": "schema_version: commandagent.pack.assist/v0\npack:\n id: ingest-default\n version: 1.0.0\n profile: ingest\n intent: create\ninject:\n - point: declare-ingest-inspection\n source: ingest_snapshot_structure_injected\n required: true\n params:\n max_files: 8\nliterals:\n - gate: ingest_source_binding\n example:\n format: json\n value: '{\"date\":\"2026-08-03\"}'\nvocabulary:\n - point: implement-ingest-delivery\n source: ingest_candidate_ids_injected\n mode: verbatim\n required: true\n params: {}\nnote: !custom hello\n", + "parser_accepts": true, + "schema_accepts": false, + "schema_error": "YAML tags are forbidden at line 24" + }, + { + "id": "merge-key-rejected", + "document": "schema_version: commandagent.pack.assist/v0\npack:\n id: ingest-default\n version: 1.0.0\n profile: ingest\n intent: create\nextra:\n <<: {a: 1}\ninject:\n - point: declare-ingest-inspection\n source: ingest_snapshot_structure_injected\n required: true\n params:\n max_files: 8\nliterals:\n - gate: ingest_source_binding\n example:\n format: json\n value: '{\"date\":\"2026-08-03\"}'\nvocabulary:\n - point: implement-ingest-delivery\n source: ingest_candidate_ids_injected\n mode: verbatim\n required: true\n params: {}\n", + "parser_accepts": true, + "schema_accepts": false, + "schema_error": "YAML merge keys are forbidden at line 8" + }, + { + "id": "multi-document-rejected", + "document": "schema_version: commandagent.pack.assist/v0\npack:\n id: ingest-default\n version: 1.0.0\n profile: ingest\n intent: create\ninject:\n - point: declare-ingest-inspection\n source: ingest_snapshot_structure_injected\n required: true\n params:\n max_files: 8\nliterals:\n - gate: ingest_source_binding\n example:\n format: json\n value: '{\"date\":\"2026-08-03\"}'\nvocabulary:\n - point: implement-ingest-delivery\n source: ingest_candidate_ids_injected\n mode: verbatim\n required: true\n params: {}\n---\nschema_version: other\n", + "parser_accepts": false, + "schema_accepts": false, + "schema_error": "deserializing from YAML containing more than one document is not supported" + }, + { + "id": "non-string-key-rejected", + "document": "1: x\nschema_version: commandagent.pack.assist/v0\npack:\n id: ingest-default\n version: 1.0.0\n profile: ingest\n intent: create\ninject:\n - point: declare-ingest-inspection\n source: ingest_snapshot_structure_injected\n required: true\n params:\n max_files: 8\nliterals:\n - gate: ingest_source_binding\n example:\n format: json\n value: '{\"date\":\"2026-08-03\"}'\nvocabulary:\n - point: implement-ingest-delivery\n source: ingest_candidate_ids_injected\n mode: verbatim\n required: true\n params: {}\n", + "parser_accepts": true, + "schema_accepts": false, + "schema_error": "unknown field `1`, expected one of `schema_version`, `pack`, `inject`, `literals`, `vocabulary`" + } + ], + "plan_cases": [ + { + "id": "valid-step-plan-accepted", + "document": "goal: Update the plan YAML guide\nsteps:\n - id: update-guide\n kind: implement\n expected_result: pass\n instruction: Update docs/guide/en/plan-yaml.md with editing guidance.\n expected_paths:\n - docs/guide/en/plan-yaml.md\n", + "accepts": true + }, + { + "id": "valid-ultra-plan-accepted", + "document": "recovery_schema_version: '1'\ngoal: recover\nprofile: generic\nstyle: recovery\nintent: recover\nphases:\n - id: repair\n prompt: Repair.\n - id: verify\n prompt: Verify.\n", + "accepts": true + }, + { + "id": "syntax-error-keeps-line-and-column", + "document": "goal: x\nsteps:\n - id: bad\n instruction: [\n", + "accepts": false, + "error_contains": "did not find expected node content", + "line": 5, + "column": 1 + }, + { + "id": "type-error-keeps-line-and-column", + "document": "goal: docs\nsteps:\n - id: update-guide\n kind: implement\n expected_result: pass\n instruction: [not, a, string]\n", + "accepts": false, + "error_contains": "invalid type: sequence, expected a string", + "line": 6, + "column": 18 + }, + { + "id": "both-steps-and-phases-rejected", + "document": "goal: x\nsteps:\n - id: s1\n instruction: a\nphases:\n - id: p1\n prompt: b\n", + "accepts": false, + "error_contains": "plan cannot contain both `steps` and `phases`", + "line": 5, + "column": 1 + }, + { + "id": "neither-steps-nor-phases-rejected", + "document": "goal: x\n", + "accepts": false, + "error_contains": "plan must contain either a `steps` list or a `phases` list", + "line": 1, + "column": 1 + }, + { + "id": "contract-error-keeps-line-and-column", + "document": "goal: test\nsteps:\n - id: verify\n kind: verify\n expected_result: pass\n instruction: Run tests.\n verify:\n - \"cargo test | cargo clippy\"\n", + "accepts": false, + "error_contains": "verify command may not use shell control syntax", + "line": 8, + "column": 10 + } + ], + "workflow_versions": [ + { + "id": "numeric-0", + "document": "workflow: recovery-circle-data\nversion: 0\nentry: create\nnodes:\n create: {intent: create, profile: generic}\nroutes: []\nterminal: {}\n", + "accepts": true + }, + { + "id": "numeric-0.1", + "document": "workflow: recovery-circle-data\nversion: 0.1\nentry: create\nnodes:\n create: {intent: create, profile: generic}\nroutes: []\nterminal: {}\n", + "accepts": true + }, + { + "id": "numeric-0.2", + "document": "workflow: recovery-circle-data\nversion: 0.2\nentry: create\nnodes:\n create: {intent: create, profile: generic}\nroutes: []\nterminal: {}\n", + "accepts": true + }, + { + "id": "numeric-0.3-rejected", + "document": "workflow: recovery-circle-data\nversion: 0.3\nentry: create\nnodes:\n create: {intent: create, profile: generic}\nroutes: []\nterminal: {}\n", + "accepts": false, + "error_contains": "workflow version must be 0, 0.1, or 0.2" + }, + { + "id": "string-zero-rejected", + "document": "workflow: recovery-circle-data\nversion: \"0\"\nentry: create\nnodes:\n create: {intent: create, profile: generic}\nroutes: []\nterminal: {}\n", + "accepts": false, + "error_contains": "workflow version must be 0, 0.1, or 0.2" + } + ], + "scrub_cases": [ + { + "id": "tagged-keys-keep-element-count", + "register_plain": [ + "SECRET-AAA-111", + "SECRET-BBB-222" + ], + "document": "!t SECRET-AAA-111: 1\n!t SECRET-BBB-222: 2\n!t keep-me: 3\n", + "mapping_len": 3, + "absent": [ + "SECRET-AAA-111", + "SECRET-BBB-222" + ], + "serialized_contains": [ + "!t keep-me: 3" + ] + }, + { + "id": "compound-mapping-keys-keep-element-count", + "register_plain": [ + "SECRET-AAA-111", + "SECRET-BBB-222" + ], + "document": "? {a: SECRET-AAA-111}\n: 1\n? {a: SECRET-BBB-222}\n: 2\n", + "mapping_len": 2, + "absent": [ + "SECRET-AAA-111", + "SECRET-BBB-222" + ] + }, + { + "id": "numeric-secret-becomes-marker-string", + "register_plain": [ + "8675309012345" + ], + "document": "port: 8675309012345\nplain: 42\n", + "mapping_len": 2, + "absent": [ + "8675309012345" + ], + "string_value": { + "port": "" + }, + "number_value": { + "plain": 42 + } + }, + { + "id": "key-collision-keeps-existing-value", + "register_plain": [ + "SECRET-AAA-111" + ], + "document": "SECRET-AAA-111: keep\n: existing\n", + "mapping_len": 2, + "absent": [ + "SECRET-AAA-111" + ], + "string_value": { + "": "existing", + "#1": "keep" + } + } + ], + "boundary": { + "max_file_bytes": 262144, + "file_error": "file exceeds 262144 bytes", + "max_depth": 16, + "depth_error": "YAML nesting exceeds 16", + "max_sequence": 256, + "sequence_error": "YAML sequence exceeds 256 entries", + "max_scalar_bytes": 65536, + "scalar_error": "YAML scalar exceeds 65536 bytes" + } +} diff --git a/tests/issue530_yaml_migration.rs b/tests/issue530_yaml_migration.rs new file mode 100644 index 00000000..3be2f16b --- /dev/null +++ b/tests/issue530_yaml_migration.rs @@ -0,0 +1,278 @@ +//! Issue #530 — the YAML parser migration compatibility corpus. +//! +//! `serde_yaml` was migrated from the unmaintained 0.9 (which pulled in +//! `unsafe-libyaml` and RUSTSEC-2024-0449) to the maintained `yaml_serde` +//! 0.10.7 (`libyaml-rs`). Because the crate name stays `serde_yaml`, no +//! production source changed. This integration test drives the public API over +//! the pre-migration measurements recorded in `fixtures/compatibility.json` and +//! requires the same accept/reject decisions, types, tags, mapping element +//! counts, and diagnostic line/column. +//! +//! The corpus fixture lives under `tests/corpus/apps/` so the generated-app +//! corpus regression discovers it; `expectations.toml` pins the fixture tokens. + +use std::collections::BTreeMap; +use std::io::Write; +use std::path::Path; + +use commandagent::planner::pack::parse_bytes; +use commandagent::planner::plan::validate_plan_file; +use commandagent::sensitive_data::{REDACTED, SecretCatalog}; +use commandagent::workflow::schema::Workflow; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +struct Compatibility { + pack_cases: Vec, + plan_cases: Vec, + workflow_versions: Vec, + scrub_cases: Vec, + boundary: Boundary, +} + +#[derive(Debug, Deserialize)] +struct PackCase { + id: String, + document: String, + parser_accepts: bool, + schema_accepts: bool, + #[serde(default)] + schema_error: Option, +} + +#[derive(Debug, Deserialize)] +struct PlanCase { + id: String, + document: String, + accepts: bool, + #[serde(default)] + error_contains: Option, + #[serde(default)] + line: Option, + #[serde(default)] + column: Option, +} + +#[derive(Debug, Deserialize)] +struct WorkflowCase { + id: String, + document: String, + accepts: bool, + #[serde(default)] + error_contains: Option, +} + +#[derive(Debug, Deserialize)] +struct ScrubCase { + id: String, + register_plain: Vec, + document: String, + mapping_len: usize, + #[serde(default)] + absent: Vec, + #[serde(default)] + serialized_contains: Vec, + #[serde(default)] + string_value: BTreeMap, + #[serde(default)] + number_value: BTreeMap, +} + +#[derive(Debug, Deserialize)] +struct Boundary { + max_file_bytes: usize, + file_error: String, + max_depth: usize, + depth_error: String, + max_sequence: usize, + sequence_error: String, + max_scalar_bytes: usize, + scalar_error: String, +} + +fn fixture() -> Compatibility { + let path = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests/corpus/apps/issue530-yaml-migration/fixtures/compatibility.json"); + let text = std::fs::read_to_string(&path) + .unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display())); + serde_json::from_str(&text) + .unwrap_or_else(|error| panic!("invalid {}: {error}", path.display())) +} + +/// The business schema must keep rejecting exactly what the YAML parser itself +/// may (or may not) accept; the two decisions are recorded separately and must +/// not collapse into one. +#[test] +fn pack_acceptance_keeps_parser_and_schema_decisions_separate() { + let compatibility = fixture(); + assert!( + !compatibility.pack_cases.is_empty(), + "no pack cases recorded" + ); + for case in &compatibility.pack_cases { + let parser = serde_yaml::from_str::(&case.document); + assert_eq!( + parser.is_ok(), + case.parser_accepts, + "{}: YAML parser accept/reject changed; error={:?}", + case.id, + parser.err().map(|error| error.to_string()) + ); + + let schema = parse_bytes(Some(case.document.as_bytes()), None); + assert_eq!( + schema.is_ok(), + case.schema_accepts, + "{}: pack schema accept/reject changed; error={:?}", + case.id, + schema.as_ref().err().map(ToString::to_string) + ); + + if let Some(expected) = &case.schema_error { + let error = schema.expect_err("negative case must reject").to_string(); + assert!(error.contains(expected), "{}: got {error}", case.id); + } + } +} + +#[test] +fn plan_diagnostics_keep_reason_and_position() { + let compatibility = fixture(); + for case in &compatibility.plan_cases { + let workspace = tempfile::tempdir().unwrap(); + let path = workspace.path().join("plan.yaml"); + std::fs::File::create(&path) + .and_then(|mut file| file.write_all(case.document.as_bytes())) + .unwrap(); + + let result = validate_plan_file(&path, workspace.path()); + assert_eq!( + result.is_ok(), + case.accepts, + "{}: plan accept/reject changed", + case.id + ); + let Err(error) = result else { + continue; + }; + let error = error.to_string(); + assert!( + error.contains(&format!( + ":{}:{}:", + case.line.expect("rejected case must record a line"), + case.column.expect("rejected case must record a column") + )), + "{}: diagnostic position lost; {error}", + case.id + ); + let expected = case + .error_contains + .as_deref() + .expect("rejected case reason"); + assert!(error.contains(expected), "{}: got {error}", case.id); + } +} + +#[test] +fn workflow_versions_keep_the_numeric_value_contract() { + let compatibility = fixture(); + for case in &compatibility.workflow_versions { + let result = Workflow::parse(&case.document); + assert_eq!(result.is_ok(), case.accepts, "{}", case.id); + if let Some(expected) = &case.error_contains { + let error = result.expect_err("negative case must reject").to_string(); + assert!(error.contains(expected), "{}: got {error}", case.id); + } + } +} + +#[test] +fn scrub_contract_keeps_type_and_mapping_element_count() { + let compatibility = fixture(); + assert!( + !compatibility.scrub_cases.is_empty(), + "no scrub cases recorded" + ); + for case in &compatibility.scrub_cases { + let mut catalog = SecretCatalog::new(); + for value in &case.register_plain { + catalog.register_plain(value).unwrap(); + } + let mut value: serde_yaml::Value = serde_yaml::from_str(&case.document) + .unwrap_or_else(|error| panic!("{}: {error}", case.id)); + catalog.scrub_yaml(&mut value); + + let mapping = value.as_mapping().unwrap(); + assert_eq!( + mapping.len(), + case.mapping_len, + "{}: element count", + case.id + ); + + let serialized = serde_yaml::to_string(&value).unwrap(); + for absent in &case.absent { + assert!(!serialized.contains(absent), "{}: leaked {absent}", case.id); + } + for expected in &case.serialized_contains { + assert!( + serialized.contains(expected), + "{}: missing {expected}; serialized={serialized}", + case.id + ); + } + for (key, expected) in &case.string_value { + assert_eq!( + value[key.as_str()].as_str(), + Some(expected.as_str()), + "{}: string value {key}", + case.id + ); + } + for (key, expected) in &case.number_value { + assert_eq!( + value[key.as_str()].as_i64(), + Some(*expected), + "{}: number value {key}", + case.id + ); + } + } + assert_eq!(REDACTED, ""); +} + +#[test] +fn boundary_limits_keep_their_recorded_errors() { + let boundary = fixture().boundary; + + let oversized = format!("#{}\n", "x".repeat(boundary.max_file_bytes + 64)); + let error = parse_bytes(Some(oversized.as_bytes()), None) + .unwrap_err() + .to_string(); + assert!(error.contains(&boundary.file_error), "{error}"); + + let mut deep = String::from("a:\n"); + for level in 1..=boundary.max_depth + 2 { + deep.push_str(&" ".repeat(level * 2)); + deep.push_str("a:\n"); + } + let error = parse_bytes(Some(deep.as_bytes()), None) + .unwrap_err() + .to_string(); + assert!(error.contains(&boundary.depth_error), "{error}"); + + let mut sequence = String::from("a:\n"); + for _ in 0..boundary.max_sequence + 8 { + sequence.push_str(" - x\n"); + } + let error = parse_bytes(Some(sequence.as_bytes()), None) + .unwrap_err() + .to_string(); + assert!(error.contains(&boundary.sequence_error), "{error}"); + + let scalar = format!("a: {}\n", "x".repeat(boundary.max_scalar_bytes + 16)); + let error = parse_bytes(Some(scalar.as_bytes()), None) + .unwrap_err() + .to_string(); + assert!(error.contains(&boundary.scalar_error), "{error}"); +}