granit-parser
Advanced tools
| name: Fuzz | ||
| on: | ||
| pull_request: | ||
| push: | ||
| branches: | ||
| - master | ||
| schedule: | ||
| - cron: "30 3 * * 0" | ||
| workflow_dispatch: | ||
| permissions: | ||
| contents: read | ||
| concurrency: | ||
| group: fuzz-${{ github.ref }} | ||
| cancel-in-progress: true | ||
| jobs: | ||
| fuzz: | ||
| name: Build and smoke-run fuzz targets | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 30 | ||
| env: | ||
| CARGO_TERM_COLOR: always | ||
| FUZZ_SECONDS: ${{ github.event_name == 'schedule' && '1200' || '20' }} | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v5 | ||
| - name: Install nightly Rust | ||
| run: rustup toolchain install nightly --profile minimal --no-self-update | ||
| - name: Rust cache | ||
| uses: Swatinem/rust-cache@v2 | ||
| - name: Install cargo-fuzz | ||
| run: cargo +nightly install cargo-fuzz | ||
| - name: Build fuzz targets | ||
| run: cargo +nightly fuzz build | ||
| - name: Smoke-run fuzz targets | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| for target in aliases_merges duplicate_keys flow_collections large_scalars; do | ||
| cargo +nightly fuzz run "${target}" -- -max_total_time="${FUZZ_SECONDS}" -timeout=10 -rss_limit_mb=4096 | ||
| done |
| use std::panic::{catch_unwind, AssertUnwindSafe}; | ||
| use granit_parser::Parser; | ||
| const CRASH_CA24C2F5B1341124FCD324CBAAFCAA9A1F6D034C: &[u8] = &[ | ||
| 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 10, 9, 35, 8, 10, 9, 255, 255, 255, 255, | ||
| 255, 10, 9, 35, 8, 35, 91, 93, 58, | ||
| ]; | ||
| fn drain_str_parser(input: &str) { | ||
| for event in Parser::new_from_str(input) { | ||
| if event.is_err() { | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| fn drain_iter_parser(input: &str) { | ||
| for event in Parser::new_from_iter(input.chars()) { | ||
| if event.is_err() { | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| #[test] | ||
| fn crash_ca24c2f5b1341124fcd324cbaafcaa9a1f6d034c_does_not_panic() { | ||
| let s = String::from_utf8_lossy(CRASH_CA24C2F5B1341124FCD324CBAAFCAA9A1F6D034C); | ||
| let inputs = [ | ||
| format!("[{s}]"), | ||
| format!("{{{s}}}"), | ||
| format!("root: {{{s}}}\narray: [{s}]\n"), | ||
| ]; | ||
| for input in inputs { | ||
| let result = catch_unwind(AssertUnwindSafe(|| drain_str_parser(&input))); | ||
| assert!(result.is_ok(), "str parser panicked for input: {input:?}"); | ||
| let result = catch_unwind(AssertUnwindSafe(|| drain_iter_parser(&input))); | ||
| assert!(result.is_ok(), "iter parser panicked for input: {input:?}"); | ||
| } | ||
| } |
| { | ||
| "git": { | ||
| "sha1": "ca296b7858b3fbd531794e7ecda5732f0bd8206f" | ||
| "sha1": "9359b0a65b0968c6c69a77138d4d007068f871f9" | ||
| }, | ||
| "path_in_vcs": "" | ||
| } |
+1
-2
@@ -115,6 +115,5 @@ # This file is automatically @generated by Cargo. | ||
| name = "granit-parser" | ||
| version = "0.0.4" | ||
| version = "0.0.5" | ||
| dependencies = [ | ||
| "arraydeque", | ||
| "clap", | ||
| "libtest-mimic", | ||
@@ -121,0 +120,0 @@ "smallvec", |
+5
-4
@@ -16,3 +16,3 @@ # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO | ||
| name = "granit-parser" | ||
| version = "0.0.4" | ||
| version = "0.0.5" | ||
| authors = [ | ||
@@ -119,2 +119,6 @@ "Ethiraric <ethiraric@gmail.com>", | ||
| [[test]] | ||
| name = "flow_collections_fuzz_regression" | ||
| path = "tests/flow_collections_fuzz_regression.rs" | ||
| [[test]] | ||
| name = "fuzz" | ||
@@ -191,5 +195,2 @@ path = "tests/fuzz.rs" | ||
| [dev-dependencies.clap] | ||
| version = "=4.5.48" | ||
| [dev-dependencies.libtest-mimic] | ||
@@ -196,0 +197,0 @@ version = "=0.8.1" |
+8
-1
| # Changelog | ||
| ## v0.0.4 | ||
| ## v0.0.5 | ||
| - Performance improvements on comment parsing | ||
| - Added `Span::tag_start` metadata for parser-emitted tagged node events, so diagnostics can point | ||
| at the explicit tag token even when the node span starts on a later line. | ||
| - Added `Tag::original_handle`, `Tag::original_parts`, and `Tag::original` so consumers can inspect | ||
| the tag handle as written before `%TAG` directive resolution and reconstruct normalized | ||
| author-facing tag spelling, including verbatim tags (breaking change). | ||
| ## v0.0.4 | ||
| Retracted | ||
@@ -6,0 +13,0 @@ ## v0.0.3 |
+61
-36
@@ -6,2 +6,3 @@ # granit-parser | ||
| [](https://github.com/bourumir-wyngs/granit-parser/actions/workflows/ci.yml) | ||
| [](https://github.com/bourumir-wyngs/granit-parser/actions/workflows/fuzz.yml) | ||
| [](https://docs.rs/granit-parser) | ||
@@ -37,3 +38,3 @@ [](https://codecov.io/gh/bourumir-wyngs/granit-parser) | ||
| [`Parser::new_from_str`](https://docs.rs/granit-parser/latest/granit_parser/struct.Parser.html#method.new_from_str) returns an iterator of ([`Event`](https://docs.rs/granit-parser/latest/granit_parser/enum.Event.html), [`Span`](https://docs.rs/granit-parser/latest/granit_parser/struct.Span.html)) pairs. The event helpers expose common node metadata, and spans provide byte ranges plus source slices: | ||
| [`Parser::new_from_str`](https://docs.rs/granit-parser/latest/granit_parser/struct.Parser.html#method.new_from_str) returns an iterator of ([`Event`](https://docs.rs/granit-parser/latest/granit_parser/enum.Event.html), [`Span`](https://docs.rs/granit-parser/latest/granit_parser/struct.Span.html)) pairs. The event helpers expose common node metadata, and spans provide byte ranges, source slices, and explicit tag-token starts for tagged nodes: | ||
@@ -47,4 +48,7 @@ Comments are emitted as `Event::Comment(text, placement)`. They are presentation metadata for tools such as linters and formatters, not YAML data nodes, so consumers that build YAML values should filter them out. The companion `Span` for a comment covers the whole source comment, including `#` and excluding the line break; when parsing from `Parser::new_from_str`, `span.slice(yaml)` returns that source comment text. | ||
| let yaml = r#" | ||
| %TAG !example! tag:example.com,2000: | ||
| --- | ||
| items: !shopping | ||
| - milk | ||
| - !example!sliced bread | ||
| - !!str bread | ||
@@ -63,9 +67,16 @@ locations: # Example with composite keys | ||
| if let Some(tag) = event.tag() { | ||
| let tag_start = span | ||
| .tag_start() | ||
| .map(|mark| (mark.line(), mark.col(), mark.byte_offset())); | ||
| if let Some((value, _style)) = event.scalar() { | ||
| println!( | ||
| "scalar tag: {tag} core-str={} for {value:?}", | ||
| "scalar tag: {tag} core-str={} tag_start(line,col,byte)={tag_start:?} for {value:?}", | ||
| tag.is_yaml_core_schema_tag("str") | ||
| ); | ||
| } else if event.is_node() { | ||
| println!("node tag: {tag} custom={}", tag.is_custom()); | ||
| println!( | ||
| "node tag: {tag} custom={} tag_start(line,col,byte)={tag_start:?}", | ||
| tag.is_custom() | ||
| ); | ||
| } | ||
@@ -89,31 +100,33 @@ } | ||
| StreamStart bytes=Some(0..0) source=Some("") | ||
| DocumentStart(false) bytes=Some(1..1) source=Some("") | ||
| MappingStart(Block, 0, None) bytes=Some(1..1) source=Some("") | ||
| Scalar("items", Plain, 0, None) bytes=Some(1..6) source=Some("items") | ||
| node tag: !shopping custom=true | ||
| SequenceStart(Block, 0, Some(Tag { handle: "!", suffix: "shopping" })) bytes=Some(20..20) source=Some("") | ||
| Scalar("milk", Plain, 0, None) bytes=Some(22..26) source=Some("milk") | ||
| scalar tag: tag:yaml.org,2002:str core-str=true for "bread" | ||
| Scalar("bread", Plain, 0, Some(Tag { handle: "tag:yaml.org,2002:", suffix: "str" })) bytes=Some(37..42) source=Some("bread") | ||
| SequenceEnd bytes=Some(43..43) source=Some("") | ||
| Scalar("locations", Plain, 0, None) bytes=Some(43..52) source=Some("locations") | ||
| Comment(" Example with composite keys", Right) bytes=Some(54..83) source=Some("# Example with composite keys") | ||
| MappingStart(Block, 0, None) bytes=Some(86..86) source=Some("") | ||
| SequenceStart(Flow, 0, None) bytes=Some(86..87) source=Some("[") | ||
| Scalar("47.3769", Plain, 0, None) bytes=Some(87..94) source=Some("47.3769") | ||
| Scalar("8.5417", Plain, 0, None) bytes=Some(96..102) source=Some("8.5417") | ||
| SequenceEnd bytes=Some(102..103) source=Some("]") | ||
| Scalar("local", Plain, 0, None) bytes=Some(105..110) source=Some("local") | ||
| SequenceStart(Flow, 0, None) bytes=Some(113..114) source=Some("[") | ||
| Scalar("40.7128", Plain, 0, None) bytes=Some(114..121) source=Some("40.7128") | ||
| Scalar("-74.0060", Plain, 0, None) bytes=Some(123..131) source=Some("-74.0060") | ||
| SequenceEnd bytes=Some(131..132) source=Some("]") | ||
| Scalar("remote", Plain, 0, None) bytes=Some(134..140) source=Some("remote") | ||
| Comment(" JSON-style \\uXXXX surrogate pairs:", Above) bytes=Some(142..178) source=Some("# JSON-style \\uXXXX surrogate pairs:") | ||
| MappingEnd bytes=Some(179..179) source=Some("") | ||
| Scalar("music", Plain, 0, None) bytes=Some(179..184) source=Some("music") | ||
| Scalar("𝄞🎵🎶", DoubleQuoted, 0, None) bytes=Some(186..224) source=Some("\"\\uD834\\uDD1E\\uD83C\\uDFB5\\uD83C\\uDFB6\"") | ||
| MappingEnd bytes=Some(225..225) source=Some("") | ||
| DocumentEnd bytes=Some(225..225) source=Some("") | ||
| StreamEnd bytes=Some(225..225) source=Some("") | ||
| DocumentStart(true) bytes=Some(38..41) source=Some("---") | ||
| MappingStart(Block, 0, None) bytes=Some(42..42) source=Some("") | ||
| Scalar("items", Plain, 0, None) bytes=Some(42..47) source=Some("items") | ||
| node tag: !shopping custom=true tag_start(line,col,byte)=Some((4, 7, Some(49))) | ||
| SequenceStart(Block, 0, Some(Tag { handle: "!", suffix: "shopping", original_handle: "!" })) bytes=Some(61..61) source=Some("") | ||
| Scalar("milk", Plain, 0, None) bytes=Some(63..67) source=Some("milk") | ||
| scalar tag: tag:example.com,2000:sliced core-str=false tag_start(line,col,byte)=Some((6, 4, Some(72))) for "bread" | ||
| Scalar("bread", Plain, 0, Some(Tag { handle: "tag:example.com,2000:", suffix: "sliced", original_handle: "!example!" })) bytes=Some(88..93) source=Some("bread") | ||
| scalar tag: tag:yaml.org,2002:str core-str=true tag_start(line,col,byte)=Some((7, 4, Some(98))) for "bread" | ||
| Scalar("bread", Plain, 0, Some(Tag { handle: "tag:yaml.org,2002:", suffix: "str", original_handle: "!!" })) bytes=Some(104..109) source=Some("bread") | ||
| SequenceEnd bytes=Some(110..110) source=Some("") | ||
| Scalar("locations", Plain, 0, None) bytes=Some(110..119) source=Some("locations") | ||
| Comment(" Example with composite keys", Right) bytes=Some(121..150) source=Some("# Example with composite keys") | ||
| MappingStart(Block, 0, None) bytes=Some(153..153) source=Some("") | ||
| SequenceStart(Flow, 0, None) bytes=Some(153..154) source=Some("[") | ||
| Scalar("47.3769", Plain, 0, None) bytes=Some(154..161) source=Some("47.3769") | ||
| Scalar("8.5417", Plain, 0, None) bytes=Some(163..169) source=Some("8.5417") | ||
| SequenceEnd bytes=Some(169..170) source=Some("]") | ||
| Scalar("local", Plain, 0, None) bytes=Some(172..177) source=Some("local") | ||
| SequenceStart(Flow, 0, None) bytes=Some(180..181) source=Some("[") | ||
| Scalar("40.7128", Plain, 0, None) bytes=Some(181..188) source=Some("40.7128") | ||
| Scalar("-74.0060", Plain, 0, None) bytes=Some(190..198) source=Some("-74.0060") | ||
| SequenceEnd bytes=Some(198..199) source=Some("]") | ||
| Scalar("remote", Plain, 0, None) bytes=Some(201..207) source=Some("remote") | ||
| Comment(" JSON-style \\uXXXX surrogate pairs:", Above) bytes=Some(209..245) source=Some("# JSON-style \\uXXXX surrogate pairs:") | ||
| MappingEnd bytes=Some(246..246) source=Some("") | ||
| Scalar("music", Plain, 0, None) bytes=Some(246..251) source=Some("music") | ||
| Scalar("𝄞🎵🎶", DoubleQuoted, 0, None) bytes=Some(253..291) source=Some("\"\\uD834\\uDD1E\\uD83C\\uDFB5\\uD83C\\uDFB6\"") | ||
| MappingEnd bytes=Some(292..292) source=Some("") | ||
| DocumentEnd bytes=Some(292..292) source=Some("") | ||
| StreamEnd bytes=Some(292..292) source=Some("") | ||
| ``` | ||
@@ -233,7 +246,19 @@ | ||
| ### Improved ergonomics | ||
| Release 0.0.3 includes ergonomic helpers such as `Event::tag`, `Event::scalar`, | ||
| `Event::anchor_id`, `Event::alias_id`, `Event::is_node`, `Tag::parts`, | ||
| `Tag::is_custom`, `Tag::is_yaml_core_schema_tag`, `Span::slice`, and | ||
| `ParserStack::push_include`. See CHANGELOG.md for details. | ||
| The following ergonomic helpers are available: | ||
| - `Event::tag` | ||
| - `Event::scalar` | ||
| - `Event::anchor_id` | ||
| - `Event::alias_id` | ||
| - `Event::is_node` | ||
| - `Tag::parts` | ||
| - `Tag::original_parts` | ||
| - `Tag::original` | ||
| - `Tag::is_custom` | ||
| - `Tag::is_yaml_core_schema_tag` | ||
| - `Span::slice` | ||
| - `Span::tag_start` | ||
| - `ParserStack::push_include` | ||
| See CHANGELOG.md for details. | ||
| ## Tools | ||
@@ -240,0 +265,0 @@ |
+9
-2
@@ -33,9 +33,16 @@ use granit_parser::{Parser, ScanError}; | ||
| if let Some(tag) = event.tag() { | ||
| let tag_start = span | ||
| .tag_start() | ||
| .map(|mark| (mark.line(), mark.col(), mark.byte_offset())); | ||
| if let Some((value, _style)) = event.scalar() { | ||
| lines.push(format!( | ||
| "scalar tag: {tag} core-str={} for {value:?}", | ||
| "scalar tag: {tag} core-str={} tag_start(line,col,byte)={tag_start:?} for {value:?}", | ||
| tag.is_yaml_core_schema_tag("str") | ||
| )); | ||
| } else if event.is_node() { | ||
| lines.push(format!("node tag: {tag} custom={}", tag.is_custom())); | ||
| lines.push(format!( | ||
| "node tag: {tag} custom={} tag_start(line,col,byte)={tag_start:?}", | ||
| tag.is_custom() | ||
| )); | ||
| } | ||
@@ -42,0 +49,0 @@ } |
+68
-0
@@ -75,2 +75,3 @@ #![allow(clippy::bool_assert_comparison)] | ||
| assert_eq!(span.byte_range(), Some(5..13)); | ||
| assert_eq!(span.tag_start(), None); | ||
@@ -86,2 +87,69 @@ let empty = granit_parser::Span::empty(Marker::new(6, 1, 6).with_byte_offset(Some(13))); | ||
| #[test] | ||
| fn tagged_block_collection_reports_tag_start_on_tag_line() { | ||
| let yaml = "key: !!omap # tag is here\n a: 1\n"; | ||
| let (_event, span) = Parser::new_from_str(yaml) | ||
| .map(Result::unwrap) | ||
| .find(|(event, _span)| { | ||
| matches!( | ||
| event, | ||
| Event::MappingStart(_, _, Some(tag)) if tag.suffix == "omap" | ||
| ) | ||
| }) | ||
| .expect("expected tagged block mapping"); | ||
| assert_eq!(span.start.line(), 2); | ||
| let tag_start = span | ||
| .tag_start() | ||
| .expect("tagged node should report tag start"); | ||
| assert_eq!(tag_start.line(), 1); | ||
| assert_eq!(tag_start.col(), 5); | ||
| assert_eq!(tag_start.byte_offset(), Some(5)); | ||
| } | ||
| #[test] | ||
| fn tag_start_uses_tag_token_when_anchor_precedes_tag() { | ||
| let yaml = "key: &a !!str value\n"; | ||
| let (_event, span) = Parser::new_from_str(yaml) | ||
| .map(Result::unwrap) | ||
| .find(|(event, _span)| { | ||
| matches!( | ||
| event, | ||
| Event::Scalar(value, _, _, Some(tag)) | ||
| if value.as_ref() == "value" && tag.suffix == "str" | ||
| ) | ||
| }) | ||
| .expect("expected tagged anchored scalar"); | ||
| let tag_start = span | ||
| .tag_start() | ||
| .expect("tagged node should report tag start"); | ||
| assert_eq!(tag_start.line(), 1); | ||
| assert_eq!(tag_start.col(), 8); | ||
| assert_eq!(tag_start.byte_offset(), Some(8)); | ||
| } | ||
| #[test] | ||
| fn tag_start_uses_tag_token_when_tag_precedes_anchor() { | ||
| let yaml = "key: !!str &a value\n"; | ||
| let (_event, span) = Parser::new_from_str(yaml) | ||
| .map(Result::unwrap) | ||
| .find(|(event, _span)| { | ||
| matches!( | ||
| event, | ||
| Event::Scalar(value, _, _, Some(tag)) | ||
| if value.as_ref() == "value" && tag.suffix == "str" | ||
| ) | ||
| }) | ||
| .expect("expected tagged anchored scalar"); | ||
| let tag_start = span | ||
| .tag_start() | ||
| .expect("tagged node should report tag start"); | ||
| assert_eq!(tag_start.line(), 1); | ||
| assert_eq!(tag_start.col(), 5); | ||
| assert_eq!(tag_start.byte_offset(), Some(5)); | ||
| } | ||
| #[test] | ||
| fn span_slice_returns_source_text_for_valid_byte_ranges() { | ||
@@ -88,0 +156,0 @@ let source = "key: value"; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display