Skip to content

Commit

Permalink
Merge pull request #92 from Byron/fuzz-test
Browse files Browse the repository at this point in the history
harden against manufactured input
  • Loading branch information
Byron authored Dec 11, 2024
2 parents ccc8a52 + a368f0f commit c61fdd4
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 98 deletions.
59 changes: 34 additions & 25 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,6 @@ where
classes,
attrs,
} => {
assert_eq!(state.current_heading, None);
state.current_heading = Some(self::Heading {
id: id.as_ref().map(|id| id.clone().into()),
classes: classes.iter().map(|class| class.clone().into()).collect(),
Expand Down Expand Up @@ -554,34 +553,40 @@ where
}
}
End(tag) => match tag {
TagEnd::Link => match state.link_stack.pop().unwrap() {
LinkCategory::AngleBracketed => formatter.write_char('>'),
LinkCategory::Reference { uri, title, id } => {
state
.shortcuts
.push((id.to_string(), uri.to_string(), title.to_string()));
formatter.write_str("][")?;
formatter.write_str(&id)?;
formatter.write_char(']')
}
LinkCategory::Collapsed { uri, title } => {
if let Some(shortcut_text) = state.current_shortcut_text.take() {
TagEnd::Link => {
let Some(category) = state.link_stack.pop() else {
// This should always be present, but people can provide any kind of event stream.
return Ok(());
};
match category {
LinkCategory::AngleBracketed => formatter.write_char('>'),
LinkCategory::Reference { uri, title, id } => {
state
.shortcuts
.push((shortcut_text, uri.to_string(), title.to_string()));
.push((id.to_string(), uri.to_string(), title.to_string()));
formatter.write_str("][")?;
formatter.write_str(&id)?;
formatter.write_char(']')
}
formatter.write_str("][]")
}
LinkCategory::Shortcut { uri, title } => {
if let Some(shortcut_text) = state.current_shortcut_text.take() {
state
.shortcuts
.push((shortcut_text, uri.to_string(), title.to_string()));
LinkCategory::Collapsed { uri, title } => {
if let Some(shortcut_text) = state.current_shortcut_text.take() {
state
.shortcuts
.push((shortcut_text, uri.to_string(), title.to_string()));
}
formatter.write_str("][]")
}
formatter.write_char(']')
LinkCategory::Shortcut { uri, title } => {
if let Some(shortcut_text) = state.current_shortcut_text.take() {
state
.shortcuts
.push((shortcut_text, uri.to_string(), title.to_string()));
}
formatter.write_char(']')
}
LinkCategory::Other { uri, title } => close_link(&uri, &title, formatter, LinkType::Inline),
}
LinkCategory::Other { uri, title } => close_link(&uri, &title, formatter, LinkType::Inline),
},
}
TagEnd::Image => match state.image_stack.pop().unwrap() {
ImageLink::Reference { uri, title, id } => {
state
Expand Down Expand Up @@ -614,11 +619,15 @@ where
TagEnd::Emphasis => formatter.write_char(options.emphasis_token),
TagEnd::Strong => formatter.write_str(options.strong_token),
TagEnd::Heading(_) => {
let Some(heading) = state.current_heading.take() else {
// Harden against manufactured input.
return Ok(());
};
let self::Heading {
id,
classes,
attributes,
} = state.current_heading.take().unwrap();
} = heading;
let emit_braces = id.is_some() || !classes.is_empty() || !attributes.is_empty();
if emit_braces {
formatter.write_str(" {")?;
Expand Down
File renamed without changes.
93 changes: 77 additions & 16 deletions tests/fmt.rs → tests/integrate/fmt.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use pulldown_cmark::{utils::TextMergeStream, Alignment, CodeBlockKind, Event, LinkType, Options, Parser, Tag, TagEnd};
use pulldown_cmark_to_cmark::{cmark, cmark_resume, cmark_resume_with_options, Options as CmarkToCmarkOptions, State};

mod source_range_fmt;
pub use pulldown_cmark_to_cmark::{
cmark, cmark_resume, cmark_resume_with_options, Options as CmarkToCmarkOptions, State,
};

fn assert_output_and_states_eq(output0: &str, state0: &State, output1: &str, state1: &State) {
assert_eq!(
Expand All @@ -16,7 +16,7 @@ fn assert_output_and_states_eq(output0: &str, state0: &State, output1: &str, sta

fn fmts_both(s: &str) -> (String, State<'_>) {
let (buf0, s0) = fmts(s);
let (buf1, s1) = source_range_fmt::fmts(s);
let (buf1, s1) = source_range::fmts(s);
assert_output_and_states_eq(&buf0, &s0, &buf1, &s1);
(buf0, s0)
}
Expand All @@ -28,7 +28,7 @@ fn fmts(s: &str) -> (String, State<'_>) {
}

fn fmts_with_options<'a>(s: &'a str, options: CmarkToCmarkOptions<'a>) -> (String, State<'a>) {
let (buf1, s1) = source_range_fmt::fmts_with_options(s, options.clone());
let (buf1, s1) = source_range::fmts_with_options(s, options.clone());
let mut buf = String::new();
let s = cmark_resume_with_options(Parser::new_ext(s, Options::all()), &mut buf, None, options).unwrap();
assert_output_and_states_eq(&buf, &s, &buf1, &s1);
Expand All @@ -49,7 +49,7 @@ fn fmte<'a>(e: impl AsRef<[Event<'a>]>) -> (String, State<'a>) {

fn assert_events_eq_both(s: &str) {
assert_events_eq(s);
source_range_fmt::assert_events_eq(s);
source_range::assert_events_eq(s);
}

/// Asserts that if we parse our `str` s into a series of events, then serialize them with `cmark`
Expand Down Expand Up @@ -170,8 +170,9 @@ mod padding {
}

mod inline_elements {
use crate::{fmts_with_options, source_range_fmt};
use crate::fmt::fmts_with_options;

use super::source_range;
use super::{fmts_both, CmarkToCmarkOptions, State};

#[test]
Expand Down Expand Up @@ -389,7 +390,7 @@ println!("Hello, world!");
{
let mut state = State::default();
state.newlines_before_start = 2;
assert_eq!(source_range_fmt::fmts("[`Vec`]"), ("[`Vec`]".into(), state));
assert_eq!(source_range::fmts("[`Vec`]"), ("[`Vec`]".into(), state));
}
}

Expand All @@ -398,11 +399,11 @@ println!("Hello, world!");
// `<` is not escaped if not escaped in the source.
let mut state = State::default();
state.newlines_before_start = 2;
assert_eq!(source_range_fmt::fmts("a < 1"), ("a < 1".into(), state));
assert_eq!(source_range::fmts("a < 1"), ("a < 1".into(), state));
// `<` is escaped if escaped in the source.
let mut state = State::default();
state.newlines_before_start = 2;
assert_eq!(source_range_fmt::fmts(r"a \< 1"), (r"a \< 1".into(), state));
assert_eq!(source_range::fmts(r"a \< 1"), (r"a \< 1".into(), state));
}
}

Expand Down Expand Up @@ -862,7 +863,8 @@ mod table {
mod escapes {
use pulldown_cmark::CowStr;

use crate::{fmts, fmts_both, source_range_fmt, CmarkToCmarkOptions, Event, Parser, Tag, TagEnd};
use super::source_range;
use crate::{fmt::fmts, fmt::fmts_both, fmt::CmarkToCmarkOptions, fmt::Event, fmt::Parser, fmt::Tag, fmt::TagEnd};

fn run_test_on_each_special_char(f: impl Fn(String, CowStr)) {
for c in CmarkToCmarkOptions::default().special_characters().chars() {
Expand All @@ -881,7 +883,7 @@ mod escapes {

#[test]
fn it_preserves_underscores_escapes() {
assert_eq!(source_range_fmt::fmts("\\_hello_world_").0, "\\_hello_world_");
assert_eq!(source_range::fmts("\\_hello_world_").0, "\\_hello_world_");
}

#[test]
Expand Down Expand Up @@ -973,7 +975,7 @@ mod escapes {
#[test]
fn it_does_not_escape_lone_square_brackets_in_text_if_the_source_does_not() {
assert_eq!(
source_range_fmt::fmts("] a closing bracket does nothing").0,
source_range::fmts("] a closing bracket does nothing").0,
"] a closing bracket does nothing"
);
}
Expand Down Expand Up @@ -1035,9 +1037,9 @@ mod escapes {

#[test]
fn entity_escape_is_not_code_block_indent() {
source_range_fmt::assert_events_eq("&#9;foo");
source_range_fmt::assert_events_eq("&#32; foo");
source_range_fmt::assert_events_eq(" * &#32; foo\n * &#9;foo");
source_range::assert_events_eq("&#9;foo");
source_range::assert_events_eq("&#32; foo");
source_range::assert_events_eq(" * &#32; foo\n * &#9;foo");
}
}

Expand Down Expand Up @@ -1375,3 +1377,62 @@ Second Term
assert_events_eq(input);
}
}

mod source_range {
// Copied from `fmt.rs`.

use pulldown_cmark::{utils::TextMergeStream, Options, Parser};
use pulldown_cmark_to_cmark::{
cmark_resume_with_source_range_and_options, cmark_with_source_range, Options as CmarkToCmarkOptions, State,
};

pub fn fmts(s: &str) -> (String, State<'_>) {
let mut buf = String::new();
let mut s = cmark_with_source_range(
Parser::new_ext(s, Options::all())
.into_offset_iter()
.map(|(e, r)| (e, Some(r))),
s,
&mut buf,
)
.unwrap();
// Not testing this field.
s.last_event_end_index = Default::default();
(buf, s)
}

pub fn fmts_with_options<'a>(s: &'a str, options: CmarkToCmarkOptions<'a>) -> (String, State<'a>) {
let mut buf = String::new();
let mut s = cmark_resume_with_source_range_and_options(
Parser::new_ext(s, Options::all())
.into_offset_iter()
.map(|(e, r)| (e, Some(r))),
s,
&mut buf,
None,
options,
)
.unwrap();
// Not testing this field.
s.last_event_end_index = Default::default();
(buf, s)
}

/// Asserts that if we parse our `str` s into a series of events, then serialize them with `cmark`
/// that we'll get the same series of events when we parse them again.
pub fn assert_events_eq(s: &str) {
let mut buf = String::new();
cmark_with_source_range(
Parser::new_ext(s, Options::all())
.into_offset_iter()
.map(|(e, r)| (e, Some(r))),
s,
&mut buf,
)
.unwrap();

let before_events = TextMergeStream::new(Parser::new_ext(s, Options::all()));
let after_events = TextMergeStream::new(Parser::new_ext(&buf, Options::all()));
assert_eq!(before_events.collect::<Vec<_>>(), after_events.collect::<Vec<_>>());
}
}
30 changes: 30 additions & 0 deletions tests/integrate.rs → tests/integrate/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,33 @@
mod display;
mod fmt;
mod spec;
mod fuzzed {
use pulldown_cmark::{Event, HeadingLevel, Tag, TagEnd};
use pulldown_cmark_to_cmark::cmark_resume;

#[test]
fn cmark_resume_with_options_does_not_panic() {
let events = [
Event::Start(Tag::Heading {
level: HeadingLevel::H2,
id: None,
classes: vec![],
attrs: vec![],
}),
Event::Start(Tag::Heading {
level: HeadingLevel::H2,
id: None,
classes: vec![],
attrs: vec![],
}),
Event::Text(pulldown_cmark::CowStr::Borrowed("(")),
Event::End(TagEnd::Heading(HeadingLevel::H2)),
Event::End(TagEnd::Heading(HeadingLevel::H2)),
];
let _ = cmark_resume(events.iter(), String::new(), None);
}
}

#[cfg(test)]
mod calculate_code_block_token_count {
use pulldown_cmark::{CodeBlockKind, CowStr, Event, Tag, TagEnd};
Expand Down
2 changes: 1 addition & 1 deletion tests/spec.rs → tests/integrate/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use pulldown_cmark::utils::TextMergeStream;
use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag, TagEnd};
use pulldown_cmark_to_cmark::cmark;

const COMMONMARK_SPEC_TEXT: &str = include_str!("./spec/CommonMark/spec.txt");
const COMMONMARK_SPEC_TEXT: &str = include_str!("../spec/CommonMark/spec.txt");

const COMMONMARK_SPEC_EXAMPLE_COUNT: usize = 649;

Expand Down
56 changes: 0 additions & 56 deletions tests/source_range_fmt.rs

This file was deleted.

0 comments on commit c61fdd4

Please sign in to comment.