mod source_edit_map;
pub use source_edit_map::PreprocessedSource;
use crate::{
DetectorRegistry, EditorLexemeKind, Error, MermaidConfig, ParseControl, ParseControlResult,
Result, SourceSpan,
};
use serde_json::{Map, Value};
use source_edit_map::{ReplacementMapping, SourceEdit};
use std::borrow::Cow;
#[cfg(test)]
use std::cell::Cell;
#[cfg(test)]
thread_local! {
static PUBLIC_PARSE_PREPROCESS_COUNT: Cell<usize> = const { Cell::new(0) };
}
#[cfg(test)]
pub(crate) fn reset_public_parse_preprocess_count() {
PUBLIC_PARSE_PREPROCESS_COUNT.set(0);
}
#[cfg(test)]
pub(crate) fn public_parse_preprocess_count() -> usize {
PUBLIC_PARSE_PREPROCESS_COUNT.get()
}
#[derive(Debug, Clone)]
pub struct PreprocessResult {
pub source: PreprocessedSource,
pub title: Option<String>,
pub config: MermaidConfig,
}
impl PreprocessResult {
pub fn code(&self) -> &str {
self.source.text()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FrontmatterByteSpan {
pub start: usize,
pub end: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FrontmatterBlockLocation<'a> {
pub full: FrontmatterByteSpan,
pub body: FrontmatterByteSpan,
pub indent: &'a str,
pub stripped: &'a str,
}
#[derive(Debug, Clone)]
pub struct FrontmatterBlock<'a> {
pub full: FrontmatterByteSpan,
pub body: FrontmatterByteSpan,
pub indent: &'a str,
pub dedented_body: Cow<'a, str>,
pub stripped: &'a str,
}
const MAX_CONFIG_NESTING_DEPTH: usize = crate::MAX_DIAGRAM_NESTING_DEPTH;
const CONTROLLED_SCAN_CHECKPOINT_BYTES: usize = 4 * 1024;
const MAX_DIRECTIVE_CONFIG_PARSE_BYTES: usize = 1024 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum DirectiveRecoveryMode {
Strict,
RecoverLine,
}
pub fn preprocess_diagram(input: &str, registry: &DetectorRegistry) -> Result<PreprocessResult> {
preprocess_diagram_with_known_type(input, registry, None)
}
pub fn preprocess_diagram_with_known_type(
input: &str,
registry: &DetectorRegistry,
diagram_type: Option<&str>,
) -> Result<PreprocessResult> {
preprocess_diagram_with_known_type_and_directive_recovery(
input,
registry,
diagram_type,
DirectiveRecoveryMode::Strict,
)
}
pub(crate) fn preprocess_diagram_with_known_type_and_directive_recovery(
input: &str,
registry: &DetectorRegistry,
diagram_type: Option<&str>,
directive_recovery: DirectiveRecoveryMode,
) -> Result<PreprocessResult> {
let control = ParseControl::new();
preprocess_diagram_with_known_type_and_directive_recovery_controlled(
input,
registry,
diagram_type,
directive_recovery,
&control,
)
.expect("a private parse control cannot be cancelled")
}
pub(crate) fn preprocess_diagram_with_known_type_and_directive_recovery_controlled(
input: &str,
registry: &DetectorRegistry,
diagram_type: Option<&str>,
directive_recovery: DirectiveRecoveryMode,
control: &ParseControl,
) -> ParseControlResult<Result<PreprocessResult>> {
let preprocessed = preprocess_single_pass_controlled(
PreprocessedSource::new_controlled(input, control)?,
registry,
diagram_type,
directive_recovery,
control,
)?;
control.checkpoint()?;
let prepared = match preprocessed {
Ok(preprocessed) => Ok(prepare_parser_code_controlled(preprocessed, control)?),
Err(error) => Err(error),
};
control.checkpoint()?;
Ok(prepared)
}
#[cfg(test)]
pub(crate) fn preprocess_mermaid_public_parse_pipeline(
input: &str,
registry: &DetectorRegistry,
diagram_type: Option<&str>,
) -> Result<PreprocessResult> {
preprocess_mermaid_public_parse_pipeline_with_directive_recovery(
input,
registry,
diagram_type,
DirectiveRecoveryMode::Strict,
)
}
#[cfg(test)]
pub(crate) fn preprocess_mermaid_public_parse_pipeline_with_directive_recovery(
input: &str,
registry: &DetectorRegistry,
diagram_type: Option<&str>,
directive_recovery: DirectiveRecoveryMode,
) -> Result<PreprocessResult> {
let control = ParseControl::new();
preprocess_mermaid_public_parse_pipeline_with_directive_recovery_controlled(
input,
registry,
diagram_type,
directive_recovery,
&control,
)
.expect("a private parse control cannot be cancelled")
}
pub(crate) fn preprocess_mermaid_public_parse_pipeline_with_directive_recovery_controlled(
input: &str,
registry: &DetectorRegistry,
diagram_type: Option<&str>,
directive_recovery: DirectiveRecoveryMode,
control: &ParseControl,
) -> ParseControlResult<Result<PreprocessResult>> {
#[cfg(test)]
PUBLIC_PARSE_PREPROCESS_COUNT.set(PUBLIC_PARSE_PREPROCESS_COUNT.get() + 1);
control.checkpoint()?;
let outer = match preprocess_single_pass_controlled(
PreprocessedSource::new_controlled(input, control)?,
registry,
diagram_type,
directive_recovery,
control,
)? {
Ok(outer) => outer,
Err(error) => return Ok(Err(error)),
};
control.checkpoint()?;
let inner = match preprocess_single_pass_controlled(
outer.source,
registry,
diagram_type,
directive_recovery,
control,
)? {
Ok(inner) => inner,
Err(error) => return Ok(Err(error)),
};
control.checkpoint()?;
let result = PreprocessResult {
source: prepare_parser_text_controlled(inner.source, control)?,
title: outer.title,
config: outer.config,
};
control.checkpoint()?;
Ok(Ok(result))
}
fn preprocess_single_pass_controlled(
mut source: PreprocessedSource,
registry: &DetectorRegistry,
diagram_type: Option<&str>,
directive_recovery: DirectiveRecoveryMode,
control: &ParseControl,
) -> ParseControlResult<Result<PreprocessResult>> {
control.checkpoint()?;
cleanup_text_controlled(&mut source, control)?;
let frontmatter = process_frontmatter_controlled(source.text(), control)?;
let (frontmatter_len, title, mut frontmatter_config) = match frontmatter {
Ok((without_frontmatter, title, config)) => (
source.text().len() - without_frontmatter.len(),
title,
config,
),
Err(error) => return Ok(Err(error)),
};
if frontmatter_len > 0 {
source.record_global_lexeme(
EditorLexemeKind::Frontmatter,
SourceSpan::new(0, frontmatter_len),
);
source.apply_edits(vec![SourceEdit::delete(0..frontmatter_len)], control)?;
}
control.checkpoint()?;
let processed_directives = match process_directives_controlled(
source.text(),
registry,
diagram_type,
directive_recovery,
control,
)? {
Ok(processed) => processed,
Err(error) => return Ok(Err(error)),
};
if processed_directives.recovered_incomplete_directive {
source.mark_recovered_incomplete_directive();
}
for prefix in processed_directives.editor_prefixes {
source.record_global_directive_prefix(prefix);
}
for removal in &processed_directives.removals {
source.record_global_lexeme(
EditorLexemeKind::Directive,
SourceSpan::new(removal.start, removal.end),
);
}
source.apply_edits(
processed_directives
.removals
.into_iter()
.map(SourceEdit::delete)
.collect(),
control,
)?;
frontmatter_config.deep_merge(processed_directives.config.as_value());
control.checkpoint()?;
remove_mermaid_comments_controlled(&mut source, control)?;
Ok(Ok(PreprocessResult {
source,
title,
config: frontmatter_config,
}))
}
fn prepare_parser_code_controlled(
mut preprocessed: PreprocessResult,
control: &ParseControl,
) -> ParseControlResult<PreprocessResult> {
preprocessed.source = prepare_parser_text_controlled(preprocessed.source, control)?;
Ok(preprocessed)
}
fn prepare_parser_text_controlled(
mut source: PreprocessedSource,
control: &ParseControl,
) -> ParseControlResult<PreprocessedSource> {
encode_mermaid_entities_like_upstream_controlled(&mut source, control)?;
Ok(source)
}
fn cleanup_text_controlled(
source: &mut PreprocessedSource,
control: &ParseControl,
) -> ParseControlResult<()> {
control.checkpoint()?;
strip_leading_utf8_bom(source, control)?;
normalize_crlf_controlled(source, control)?;
normalize_html_tag_attributes_like_upstream_controlled(source, control)?;
control.checkpoint()
}
fn strip_leading_utf8_bom(
source: &mut PreprocessedSource,
control: &ParseControl,
) -> ParseControlResult<()> {
if source.text().starts_with('\u{feff}') {
source.apply_edits(vec![SourceEdit::delete(0..'\u{feff}'.len_utf8())], control)?;
}
Ok(())
}
fn remove_mermaid_comments_controlled(
source: &mut PreprocessedSource,
control: &ParseControl,
) -> ParseControlResult<()> {
let text = source.text();
let mut checkpoints = ControlledScanCheckpoints::new(control)?;
let mut edits = Vec::new();
let mut comments = Vec::new();
let mut line_start = 0usize;
while line_start < text.len() {
let newline = find_newline_controlled(text, line_start, &mut checkpoints)?;
let line_end = newline.map_or(text.len(), |newline| newline + 1);
let line = &text[line_start..line_end];
let trimmed_start = trim_start_whitespace_controlled(line, &mut checkpoints)?;
let trimmed = &line[trimmed_start..];
if let Some(after_marker) = trimmed.strip_prefix("%%") {
let has_comment_body = after_marker.chars().next().is_some_and(|ch| ch != '\n');
if !after_marker.starts_with('{') && has_comment_body {
let range = line_start..line_end;
comments.push(SourceSpan::new(range.start, range.end));
edits.push(SourceEdit::delete(range));
}
}
let Some(newline) = newline else {
break;
};
line_start = newline + 1;
}
checkpoints.finish()?;
for (index, span) in comments.into_iter().enumerate() {
if index.is_multiple_of(128) {
control.checkpoint()?;
}
source.record_global_lexeme(EditorLexemeKind::Comment, span);
}
source.apply_edits(edits, control)?;
let mut checkpoints = ControlledScanCheckpoints::new(control)?;
let leading_whitespace = trim_start_whitespace_controlled(source.text(), &mut checkpoints)?;
checkpoints.finish()?;
if leading_whitespace > 0 {
source.apply_edits(vec![SourceEdit::delete(0..leading_whitespace)], control)?;
}
control.checkpoint()
}
fn normalize_crlf_controlled(
source: &mut PreprocessedSource,
control: &ParseControl,
) -> ParseControlResult<()> {
let bytes = source.text().as_bytes();
let mut checkpoints = ControlledScanCheckpoints::new(control)?;
let mut edits = Vec::new();
let mut cursor = 0usize;
while cursor < bytes.len() {
checkpoints.scanned(1)?;
if bytes[cursor] == b'\r' {
let end = cursor + usize::from(bytes.get(cursor + 1) == Some(&b'\n')) + 1;
edits.push(SourceEdit::replace(
cursor..end,
"\n",
if end - cursor == 1 {
ReplacementMapping::ExactBytes
} else {
ReplacementMapping::Boundaries
},
));
cursor = end;
} else {
cursor += 1;
}
}
checkpoints.finish()?;
source.apply_edits(edits, control)?;
control.checkpoint()
}
#[cfg(test)]
fn normalize_crlf(source: &mut PreprocessedSource) {
normalize_crlf_controlled(source, &ParseControl::new())
.expect("a private parse control cannot be cancelled");
}
fn normalize_html_tag_attributes_like_upstream_controlled(
source: &mut PreprocessedSource,
control: &ParseControl,
) -> ParseControlResult<()> {
let text = source.text();
let bytes = text.as_bytes();
let mut checkpoints = ControlledScanCheckpoints::new(control)?;
let mut probe = 0usize;
let mut edits = Vec::new();
while let Some(start) = find_ascii_pattern_controlled(text, probe, b"<", &mut checkpoints)? {
let tag_start = start + 1;
if tag_start >= bytes.len() || !is_mermaid_js_word_byte(bytes[tag_start]) {
probe = tag_start;
continue;
}
let mut tag_end = tag_start + 1;
while tag_end < bytes.len() && is_mermaid_js_word_byte(bytes[tag_end]) {
checkpoints.scanned(1)?;
tag_end += 1;
}
let Some(end) = find_ascii_pattern_controlled(text, tag_end, b">", &mut checkpoints)?
else {
break;
};
html_attribute_quote_edits_controlled(text, tag_end, end, &mut edits, &mut checkpoints)?;
probe = end + 1;
}
checkpoints.finish()?;
source.apply_edits(edits, control)?;
control.checkpoint()
}
#[cfg(test)]
fn normalize_html_tag_attributes_like_upstream(source: &mut PreprocessedSource) {
normalize_html_tag_attributes_like_upstream_controlled(source, &ParseControl::new())
.expect("a private parse control cannot be cancelled");
}
fn html_attribute_quote_edits_controlled(
text: &str,
attributes_start: usize,
attributes_end: usize,
edits: &mut Vec<SourceEdit>,
checkpoints: &mut ControlledScanCheckpoints<'_>,
) -> ParseControlResult<()> {
let mut probe = attributes_start;
while let Some(start) =
find_ascii_pattern_in_range_controlled(text, probe, attributes_end, b"=\"", checkpoints)?
{
let value_start = start + 2;
let Some(end) = find_ascii_pattern_in_range_controlled(
text,
value_start,
attributes_end,
b"\"",
checkpoints,
)?
else {
probe = value_start;
continue;
};
let opening_quote = start + 1;
let closing_quote = end;
edits.push(SourceEdit::replace(
opening_quote..opening_quote + 1,
"'",
ReplacementMapping::ExactBytes,
));
edits.push(SourceEdit::replace(
closing_quote..closing_quote + 1,
"'",
ReplacementMapping::ExactBytes,
));
probe = end + 1;
}
Ok(())
}
fn encode_mermaid_entities_like_upstream_controlled(
source: &mut PreprocessedSource,
control: &ParseControl,
) -> ParseControlResult<()> {
strip_hex_style_semicolons_like_upstream_controlled(source, "style", control)?;
strip_hex_style_semicolons_like_upstream_controlled(source, "classDef", control)?;
encode_entity_placeholders_like_upstream_controlled(source, control)?;
control.checkpoint()
}
#[cfg(test)]
fn encode_mermaid_entities_like_upstream(source: &mut PreprocessedSource) {
encode_mermaid_entities_like_upstream_controlled(source, &ParseControl::new())
.expect("a private parse control cannot be cancelled");
}
fn encode_entity_placeholders_like_upstream_controlled(
source: &mut PreprocessedSource,
control: &ParseControl,
) -> ParseControlResult<()> {
let text = source.text();
let bytes = text.as_bytes();
let mut checkpoints = ControlledScanCheckpoints::new(control)?;
let mut cursor = 0usize;
let mut edits = Vec::new();
while let Some(start) = find_ascii_pattern_controlled(text, cursor, b"#", &mut checkpoints)? {
let mut end = start + 1;
while end < bytes.len() && is_mermaid_js_word_byte(bytes[end]) {
checkpoints.scanned(1)?;
end += 1;
}
if end > start + 1 && bytes.get(end) == Some(&b';') {
let inner = &text[start + 1..end];
let mut all_digits = true;
for byte in inner.bytes() {
checkpoints.scanned(1)?;
if !byte.is_ascii_digit() {
all_digits = false;
break;
}
}
let prefix = if all_digits { "fl°°" } else { "fl°" };
edits.push(SourceEdit::replace(
start..start + 1,
prefix,
ReplacementMapping::Boundaries,
));
edits.push(SourceEdit::replace(
end..end + 1,
"¶ß",
ReplacementMapping::Boundaries,
));
cursor = end + 1;
} else {
cursor = start + 1;
}
}
checkpoints.finish()?;
source.apply_edits(edits, control)?;
control.checkpoint()
}
fn is_mermaid_js_word_byte(byte: u8) -> bool {
byte.is_ascii_alphanumeric() || byte == b'_'
}
fn strip_hex_style_semicolons_like_upstream_controlled(
source: &mut PreprocessedSource,
keyword: &str,
control: &ParseControl,
) -> ParseControlResult<()> {
let text = source.text();
let mut checkpoints = ControlledScanCheckpoints::new(control)?;
let mut edits = Vec::new();
let mut line_start = 0usize;
loop {
let newline = find_newline_controlled(text, line_start, &mut checkpoints)?;
let line_end = newline.unwrap_or(text.len());
collect_hex_style_semicolon_edits_controlled(
text,
line_start,
line_end,
keyword,
&mut edits,
&mut checkpoints,
)?;
let Some(newline) = newline else {
break;
};
line_start = newline + 1;
}
checkpoints.finish()?;
source.apply_edits(edits, control)?;
control.checkpoint()
}
fn collect_hex_style_semicolon_edits_controlled(
text: &str,
line_start: usize,
line_end: usize,
keyword: &str,
edits: &mut Vec<SourceEdit>,
checkpoints: &mut ControlledScanCheckpoints<'_>,
) -> ParseControlResult<()> {
let mut cursor = line_start;
while let Some(semicolon) =
find_hex_style_match_controlled(text, line_start, line_end, keyword, cursor, checkpoints)?
{
edits.push(SourceEdit::delete(semicolon..semicolon + 1));
cursor = semicolon + 1;
}
Ok(())
}
fn find_hex_style_match_controlled(
text: &str,
line_start: usize,
line_end: usize,
keyword: &str,
search_start: usize,
checkpoints: &mut ControlledScanCheckpoints<'_>,
) -> ParseControlResult<Option<usize>> {
let Some(start) = find_ascii_pattern_in_range_controlled(
text,
search_start.max(line_start),
line_end,
keyword.as_bytes(),
checkpoints,
)?
else {
return Ok(None);
};
find_hex_style_match_end_controlled(text, start + keyword.len(), line_end, checkpoints)
}
fn find_hex_style_match_end_controlled(
text: &str,
search_start: usize,
line_end: usize,
checkpoints: &mut ControlledScanCheckpoints<'_>,
) -> ParseControlResult<Option<usize>> {
let mut probe = search_start;
while let Some(colon) =
find_ascii_pattern_in_range_controlled(text, probe, line_end, b":", checkpoints)?
{
let mut hash = None;
let mut token_end = colon + 1;
for (rel, ch) in text[colon + 1..line_end].char_indices() {
checkpoints.scanned(ch.len_utf8())?;
if ch.is_whitespace() {
token_end = colon + 1 + rel;
break;
}
token_end = colon + 1 + rel + ch.len_utf8();
if ch == '#' {
hash = Some(colon + 1 + rel);
break;
}
}
if let Some(hash) = hash {
return rfind_ascii_byte_in_range_controlled(
text,
hash + 1,
line_end,
b';',
checkpoints,
);
}
probe = token_end.max(colon + 1);
}
Ok(None)
}
fn rfind_ascii_byte_in_range_controlled(
text: &str,
start: usize,
end: usize,
needle: u8,
checkpoints: &mut ControlledScanCheckpoints<'_>,
) -> ParseControlResult<Option<usize>> {
for offset in (start..end.min(text.len())).rev() {
checkpoints.scanned(1)?;
if text.as_bytes()[offset] == needle {
return Ok(Some(offset));
}
}
Ok(None)
}
fn process_frontmatter_controlled<'a>(
input: &'a str,
control: &ParseControl,
) -> ParseControlResult<Result<(&'a str, Option<String>, MermaidConfig)>> {
control.checkpoint()?;
let Some(frontmatter) = split_frontmatter_block_controlled(input, control)? else {
return Ok(Ok((input, None, MermaidConfig::empty_object())));
};
let yaml_body = frontmatter.dedented_body;
let stripped = frontmatter.stripped;
if config_nesting_exceeds_limit_controlled(yaml_body.as_ref(), control)? {
return Ok(Err(Error::InvalidFrontMatterYaml {
message: format!("config nesting exceeds {MAX_CONFIG_NESTING_DEPTH} levels"),
}));
}
control.checkpoint()?;
let parsed_obj = match parse_frontmatter_yaml_fields_controlled(yaml_body.as_ref(), control)? {
Ok(parsed) => parsed,
Err(message) => return Ok(Err(Error::InvalidFrontMatterYaml { message })),
};
if let Err(cancelled) = control.checkpoint() {
crate::config::drop_value_nonrecursive(Value::Object(parsed_obj));
return Err(cancelled);
}
let mut title = None;
let mut display_mode = None;
if let Some(t) = parsed_obj
.get("title")
.filter(|value| frontmatter_truthy(value))
{
title = Some(frontmatter_to_string(t));
}
if let Some(dm) = parsed_obj
.get("displayMode")
.filter(|value| frontmatter_truthy(value))
{
display_mode = Some(frontmatter_to_string(dm));
}
let mut config = MermaidConfig::empty_object();
merge_top_level_frontmatter_diagram_configs(&parsed_obj, &mut config);
if let Some(v) = parsed_obj
.get("config")
.filter(|value| frontmatter_truthy(value))
{
config.deep_merge(v);
}
crate::config::mirror_legacy_font_family_into_theme_variables(&mut config);
if let Some(dm) = display_mode {
config.set_value("gantt.displayMode", Value::String(dm));
}
crate::config::drop_value_nonrecursive(Value::Object(parsed_obj));
control.checkpoint()?;
Ok(Ok((stripped, title, config)))
}
pub fn split_frontmatter_block(input: &str) -> Option<FrontmatterBlock<'_>> {
let control = ParseControl::new();
split_frontmatter_block_controlled(input, &control)
.expect("a private parse control cannot be cancelled")
}
pub fn locate_frontmatter_block_controlled<'a>(
input: &'a str,
control: &ParseControl,
) -> ParseControlResult<Option<FrontmatterBlockLocation<'a>>> {
let mut checkpoints = ControlledScanCheckpoints::new(control)?;
let Some(open_line_newline) = find_newline_controlled(input, 0, &mut checkpoints)? else {
checkpoints.finish()?;
return Ok(None);
};
let open_line_end =
trim_frontmatter_line_end_controlled(input, 0, open_line_newline, b"\r", &mut checkpoints)?;
let open_line = &input[..open_line_end];
let indent_end = frontmatter_indent_end_controlled(open_line, &mut checkpoints)?;
let indent = &open_line[..indent_end];
let after_indent = &open_line[indent_end..];
if !after_indent.starts_with("---")
|| !frontmatter_is_whitespace_controlled(&after_indent[3..], &mut checkpoints)?
{
checkpoints.finish()?;
return Ok(None);
}
let body_start = open_line_newline + 1;
let mut line_start = body_start;
while line_start < input.len() {
let line_end_with_newline = find_newline_controlled(input, line_start, &mut checkpoints)?
.map_or(input.len(), |newline| newline + 1);
let line_end = trim_frontmatter_line_end_controlled(
input,
line_start,
line_end_with_newline,
b"\r\n",
&mut checkpoints,
)?;
let line = &input[line_start..line_end];
if is_frontmatter_closing_line_controlled(line, indent, &mut checkpoints)? {
let body_end = if line_start > body_start
&& input.as_bytes().get(line_start - 1) == Some(&b'\n')
{
line_start - 1
} else {
line_start
};
let stripped = &input[line_end_with_newline..];
let location = FrontmatterBlockLocation {
full: FrontmatterByteSpan {
start: 0,
end: line_end_with_newline,
},
body: FrontmatterByteSpan {
start: body_start,
end: body_end,
},
indent,
stripped,
};
checkpoints.finish()?;
return Ok(Some(location));
}
if line_end_with_newline == input.len() {
break;
}
line_start = line_end_with_newline;
}
checkpoints.finish()?;
Ok(None)
}
pub fn split_frontmatter_block_controlled<'a>(
input: &'a str,
control: &ParseControl,
) -> ParseControlResult<Option<FrontmatterBlock<'a>>> {
let Some(location) = locate_frontmatter_block_controlled(input, control)? else {
return Ok(None);
};
let body = &input[location.body.start..location.body.end];
let dedented_body = dedent_frontmatter_body_controlled(body, location.indent, control)?;
control.checkpoint()?;
Ok(Some(FrontmatterBlock {
full: location.full,
body: location.body,
indent: location.indent,
dedented_body,
stripped: location.stripped,
}))
}
pub fn parse_frontmatter_yaml_fields(
input: &str,
) -> std::result::Result<Map<String, Value>, String> {
let control = ParseControl::new();
parse_frontmatter_yaml_fields_controlled(input, &control)
.expect("a private parse control cannot be cancelled")
}
pub fn parse_frontmatter_yaml_fields_controlled(
input: &str,
control: &ParseControl,
) -> ParseControlResult<std::result::Result<Map<String, Value>, String>> {
let parsed =
crate::yaml_config::parse_yaml_value_controlled(input, MAX_CONFIG_NESTING_DEPTH, control)?;
Ok(frontmatter_fields_from_yaml_value(parsed))
}
pub fn parse_frontmatter_yaml_fields_bounded_controlled(
input: &str,
max_input_bytes: usize,
max_nesting_depth: usize,
max_materialized_bytes: usize,
control: &ParseControl,
) -> ParseControlResult<std::result::Result<Map<String, Value>, String>> {
let parsed = crate::yaml_config::parse_yaml_value_with_limits_controlled(
input,
max_input_bytes,
max_nesting_depth,
max_materialized_bytes,
control,
)?;
Ok(frontmatter_fields_from_yaml_value(parsed))
}
fn frontmatter_fields_from_yaml_value(
parsed: std::result::Result<Value, String>,
) -> std::result::Result<Map<String, Value>, String> {
let parsed = parsed?;
match parsed {
Value::Object(map) => Ok(map),
other => {
crate::config::drop_value_nonrecursive(other);
Ok(Map::new())
}
}
}
pub fn diagram_config_key_for_type(diagram_type: &str) -> &str {
crate::family::config_namespace_for_diagram_type(diagram_type).unwrap_or(diagram_type)
}
struct ControlledScanCheckpoints<'a> {
control: &'a ParseControl,
bytes_since_checkpoint: usize,
}
impl<'a> ControlledScanCheckpoints<'a> {
fn new(control: &'a ParseControl) -> ParseControlResult<Self> {
control.checkpoint()?;
Ok(Self {
control,
bytes_since_checkpoint: 0,
})
}
fn scanned(&mut self, bytes: usize) -> ParseControlResult<()> {
self.bytes_since_checkpoint = self.bytes_since_checkpoint.saturating_add(bytes);
while self.bytes_since_checkpoint >= CONTROLLED_SCAN_CHECKPOINT_BYTES {
self.control.checkpoint()?;
self.bytes_since_checkpoint -= CONTROLLED_SCAN_CHECKPOINT_BYTES;
}
Ok(())
}
fn finish(&self) -> ParseControlResult<()> {
self.control.checkpoint()
}
}
fn find_newline_controlled(
input: &str,
start: usize,
checkpoints: &mut ControlledScanCheckpoints<'_>,
) -> ParseControlResult<Option<usize>> {
for (offset, byte) in input.as_bytes()[start..].iter().enumerate() {
checkpoints.scanned(1)?;
if *byte == b'\n' {
return Ok(Some(start + offset));
}
}
Ok(None)
}
fn trim_frontmatter_line_end_controlled(
input: &str,
start: usize,
mut end: usize,
trim: &[u8],
checkpoints: &mut ControlledScanCheckpoints<'_>,
) -> ParseControlResult<usize> {
while end > start && trim.contains(&input.as_bytes()[end - 1]) {
checkpoints.scanned(1)?;
end -= 1;
}
Ok(end)
}
fn frontmatter_indent_end_controlled(
line: &str,
checkpoints: &mut ControlledScanCheckpoints<'_>,
) -> ParseControlResult<usize> {
let mut end = 0usize;
for (idx, ch) in line.char_indices() {
checkpoints.scanned(ch.len_utf8())?;
if ch == '\n' || ch == '\r' || !ch.is_whitespace() {
break;
}
end = idx + ch.len_utf8();
}
Ok(end)
}
fn is_frontmatter_closing_line_controlled(
line: &str,
indent: &str,
checkpoints: &mut ControlledScanCheckpoints<'_>,
) -> ParseControlResult<bool> {
if !frontmatter_has_prefix_controlled(line, indent, checkpoints)? {
return Ok(false);
}
let after_indent = &line[indent.len()..];
Ok(after_indent.starts_with("---")
&& frontmatter_is_whitespace_controlled(&after_indent[3..], checkpoints)?)
}
fn frontmatter_has_prefix_controlled(
text: &str,
prefix: &str,
checkpoints: &mut ControlledScanCheckpoints<'_>,
) -> ParseControlResult<bool> {
if text.len() < prefix.len() {
return Ok(false);
}
for (actual, expected) in text.as_bytes()[..prefix.len()]
.iter()
.zip(prefix.as_bytes())
{
checkpoints.scanned(1)?;
if actual != expected {
return Ok(false);
}
}
Ok(true)
}
fn frontmatter_is_whitespace_controlled(
text: &str,
checkpoints: &mut ControlledScanCheckpoints<'_>,
) -> ParseControlResult<bool> {
for ch in text.chars() {
checkpoints.scanned(ch.len_utf8())?;
if !ch.is_whitespace() {
return Ok(false);
}
}
Ok(true)
}
fn dedent_frontmatter_body_controlled<'a>(
body: &'a str,
indent: &str,
control: &ParseControl,
) -> ParseControlResult<Cow<'a, str>> {
if indent.is_empty() {
control.checkpoint()?;
return Ok(Cow::Borrowed(body));
}
let mut checkpoints = ControlledScanCheckpoints::new(control)?;
let mut out = String::with_capacity(body.len());
let mut line_start = 0usize;
loop {
let line_end =
find_newline_controlled(body, line_start, &mut checkpoints)?.unwrap_or(body.len());
let line = &body[line_start..line_end];
let content = if frontmatter_has_prefix_controlled(line, indent, &mut checkpoints)? {
&line[indent.len()..]
} else {
line
};
push_frontmatter_str_controlled(&mut out, content, &mut checkpoints)?;
if line_end == body.len() {
break;
}
out.push('\n');
line_start = line_end + 1;
}
checkpoints.finish()?;
Ok(Cow::Owned(out))
}
fn push_frontmatter_str_controlled(
out: &mut String,
text: &str,
checkpoints: &mut ControlledScanCheckpoints<'_>,
) -> ParseControlResult<()> {
let mut chunk_start = 0usize;
let mut chunk_len = 0usize;
for (idx, ch) in text.char_indices() {
let ch_len = ch.len_utf8();
checkpoints.scanned(ch_len)?;
chunk_len += ch_len;
if chunk_len >= CONTROLLED_SCAN_CHECKPOINT_BYTES {
let chunk_end = idx + ch_len;
out.push_str(&text[chunk_start..chunk_end]);
chunk_start = chunk_end;
chunk_len = 0;
}
}
out.push_str(&text[chunk_start..]);
Ok(())
}
fn frontmatter_truthy(value: &Value) -> bool {
match value {
Value::Null => false,
Value::Bool(value) => *value,
Value::Number(value) => value.as_f64().is_some_and(|value| value != 0.0),
Value::String(value) => !value.is_empty(),
Value::Array(_) | Value::Object(_) => true,
}
}
fn frontmatter_to_string(value: &Value) -> String {
match value {
Value::String(value) => value.clone(),
Value::Number(value) => value.to_string(),
Value::Bool(true) => "true".to_string(),
Value::Bool(false) => "false".to_string(),
Value::Null => "null".to_string(),
Value::Array(_) | Value::Object(_) => value.to_string(),
}
}
fn merge_top_level_frontmatter_diagram_configs(
parsed_obj: &serde_json::Map<String, Value>,
config: &mut MermaidConfig,
) {
for fact in crate::family::frontmatter_config_aliases() {
if let Some(value) = parsed_obj.get(fact.source) {
config.set_value(
fact.namespace,
crate::config::clone_value_nonrecursive(value),
);
}
}
for &key in crate::family::frontmatter_config_namespaces() {
if let Some(value) = parsed_obj.get(key) {
config.set_value(key, crate::config::clone_value_nonrecursive(value));
}
}
}
struct ProcessedDirectives {
config: MermaidConfig,
removals: Vec<std::ops::Range<usize>>,
editor_prefixes: Vec<String>,
recovered_incomplete_directive: bool,
}
fn process_directives_controlled(
input: &str,
registry: &DetectorRegistry,
diagram_type: Option<&str>,
directive_recovery: DirectiveRecoveryMode,
control: &ParseControl,
) -> ParseControlResult<Result<ProcessedDirectives>> {
control.checkpoint()?;
let blocks = directive_blocks_controlled(input, directive_recovery, control)?;
if blocks.is_empty() {
return Ok(Ok(ProcessedDirectives {
config: MermaidConfig::empty_object(),
removals: Vec::new(),
editor_prefixes: Vec::new(),
recovered_incomplete_directive: false,
}));
}
let recovered_incomplete_directive = blocks.iter().any(|block| block.raw.is_none());
let mut directives = Vec::new();
for (index, block) in blocks.iter().enumerate() {
if index % 32 == 0 {
control.checkpoint()?;
}
let Some(raw) = block.raw else {
continue;
};
match parse_directive_like_upstream_controlled(raw, control)? {
Ok(Some(directive)) => directives.push(directive),
Ok(None) => {}
Err(error) => return Ok(Err(error)),
}
}
control.checkpoint()?;
let input_without_directives = remove_directive_blocks_controlled(input, &blocks, control)?;
let init = match detect_init_controlled(
&directives,
input_without_directives.as_ref(),
registry,
diagram_type,
control,
)? {
Ok(init) => init,
Err(error) => return Ok(Err(error)),
};
let wrap = directives.iter().any(|d| d.ty == "wrap");
let mut editor_prefixes = Vec::new();
for (index, directive) in directives.iter().enumerate() {
if index % 32 == 0 {
control.checkpoint()?;
}
if matches!(directive.ty.as_str(), "init" | "initialize" | "wrap")
&& !editor_prefixes.contains(&directive.ty)
{
editor_prefixes.push(directive.ty.clone());
}
}
let mut merged = init;
if wrap {
merged.set_value("wrap", Value::Bool(true));
}
control.checkpoint()?;
Ok(Ok(ProcessedDirectives {
config: merged,
removals: blocks.into_iter().map(|block| block.range).collect(),
editor_prefixes,
recovered_incomplete_directive,
}))
}
#[cfg(test)]
fn detect_init(
directives: &[Directive],
input: &str,
registry: &DetectorRegistry,
diagram_type: Option<&str>,
) -> Result<MermaidConfig> {
let control = ParseControl::new();
detect_init_controlled(directives, input, registry, diagram_type, &control)
.expect("a private parse control cannot be cancelled")
}
fn detect_init_controlled(
directives: &[Directive],
input: &str,
registry: &DetectorRegistry,
diagram_type: Option<&str>,
control: &ParseControl,
) -> ParseControlResult<Result<MermaidConfig>> {
control.checkpoint()?;
let mut merged = MermaidConfig::empty_object();
let mut config_for_detect = MermaidConfig::empty_object();
let mut detected_type = diagram_type.map(str::to_owned);
let mut detection_attempted = diagram_type.is_some();
for (index, d) in directives.iter().enumerate() {
if index % 16 == 0 {
control.checkpoint()?;
}
if d.ty != "init" && d.ty != "initialize" {
continue;
}
let mut args = match &d.args {
Some(v) => crate::config::clone_value_nonrecursive(v),
None => Value::Object(Default::default()),
};
let mut diagram_specific = args
.as_object_mut()
.and_then(|object| object.remove("config"));
sanitize_directive_controlled(&mut args, control)?;
if let Some(mut diagram_specific_value) = diagram_specific.take() {
sanitize_directive_controlled(&mut diagram_specific_value, control)?;
if !detection_attempted {
detection_attempted = true;
detected_type = match registry.detect_type_controlled(
input,
&mut config_for_detect,
control,
)? {
Ok(diagram_type) => Some(diagram_type.to_string()),
Err(_) => None,
};
}
if let Some(ty) = detected_type.as_deref() {
let key = diagram_config_key_for_type(ty).to_string();
if let Value::Object(obj) = &mut args
&& let Some(old) = obj.insert(key, diagram_specific_value)
{
crate::config::drop_value_nonrecursive(old);
}
} else {
crate::config::drop_value_nonrecursive(diagram_specific_value);
}
}
crate::config::mirror_legacy_font_family_into_theme_variables_value(&mut args);
merged.deep_merge(&args);
}
control.checkpoint()?;
Ok(Ok(merged))
}
#[derive(Debug, Clone)]
struct Directive {
ty: String,
args: Option<Value>,
}
#[derive(Debug)]
struct DirectiveBlock<'a> {
raw: Option<&'a str>,
range: std::ops::Range<usize>,
}
fn remove_directive_blocks_controlled<'a>(
input: &'a str,
blocks: &[DirectiveBlock<'_>],
control: &ParseControl,
) -> ParseControlResult<Cow<'a, str>> {
if blocks.is_empty() {
control.checkpoint()?;
return Ok(Cow::Borrowed(input));
}
let mut retained_bytes = input.len();
for (index, block) in blocks.iter().enumerate() {
if index.is_multiple_of(32) {
control.checkpoint()?;
}
retained_bytes = retained_bytes.saturating_sub(block.range.len());
}
let mut output = String::with_capacity(retained_bytes);
let mut checkpoints = ControlledScanCheckpoints::new(control)?;
let mut cursor = 0usize;
for block in blocks {
push_frontmatter_str_controlled(
&mut output,
&input[cursor..block.range.start],
&mut checkpoints,
)?;
cursor = block.range.end;
}
push_frontmatter_str_controlled(&mut output, &input[cursor..], &mut checkpoints)?;
checkpoints.finish()?;
Ok(Cow::Owned(output))
}
#[cfg(test)]
fn directive_blocks(
input: &str,
directive_recovery: DirectiveRecoveryMode,
) -> Vec<DirectiveBlock<'_>> {
let control = ParseControl::new();
directive_blocks_controlled(input, directive_recovery, &control)
.expect("a private parse control cannot be cancelled")
}
fn directive_blocks_controlled<'a>(
input: &'a str,
directive_recovery: DirectiveRecoveryMode,
control: &ParseControl,
) -> ParseControlResult<Vec<DirectiveBlock<'a>>> {
let mut checkpoints = ControlledScanCheckpoints::new(control)?;
let mut blocks = Vec::new();
let mut pos = 0;
while let Some(start) = find_ascii_pattern_controlled(input, pos, b"%%{", &mut checkpoints)? {
let content_start = start + 3;
let close = find_ascii_pattern_controlled(input, content_start, b"}%%", &mut checkpoints)?;
let next_open = if directive_recovery == DirectiveRecoveryMode::RecoverLine {
find_ascii_pattern_controlled(input, content_start, b"%%{", &mut checkpoints)?
} else {
None
};
let content_end = match directive_recovery {
DirectiveRecoveryMode::Strict => close,
DirectiveRecoveryMode::RecoverLine => {
close.filter(|close| next_open.is_none_or(|open| *close < open))
}
};
if let Some(content_end) = content_end {
let end = content_end + 3;
let raw = &input[content_start..content_end];
let (trimmed_start, trimmed_end) =
trim_whitespace_bounds_controlled(raw, &mut checkpoints)?;
blocks.push(DirectiveBlock {
raw: Some(&raw[trimmed_start..trimmed_end]),
range: start..end,
});
pos = end;
continue;
}
let end = match directive_recovery {
DirectiveRecoveryMode::Strict => input.len(),
DirectiveRecoveryMode::RecoverLine => {
let line_end = find_line_break_controlled(input, content_start, &mut checkpoints)?
.unwrap_or(input.len());
next_open.map_or(line_end, |open| line_end.min(open))
}
};
blocks.push(DirectiveBlock {
raw: None,
range: start..end,
});
pos = end;
}
checkpoints.finish()?;
Ok(blocks)
}
fn find_ascii_pattern_controlled(
input: &str,
start: usize,
pattern: &[u8],
checkpoints: &mut ControlledScanCheckpoints<'_>,
) -> ParseControlResult<Option<usize>> {
find_ascii_pattern_in_range_controlled(input, start, input.len(), pattern, checkpoints)
}
fn find_ascii_pattern_in_range_controlled(
input: &str,
start: usize,
end: usize,
pattern: &[u8],
checkpoints: &mut ControlledScanCheckpoints<'_>,
) -> ParseControlResult<Option<usize>> {
let end = end.min(input.len());
if pattern.is_empty() || start > end || pattern.len() > end.saturating_sub(start) {
return Ok(None);
}
let bytes = input.as_bytes();
let last_start = end - pattern.len();
for cursor in start..=last_start {
checkpoints.scanned(1)?;
if &bytes[cursor..cursor + pattern.len()] == pattern {
return Ok(Some(cursor));
}
}
Ok(None)
}
fn find_line_break_controlled(
input: &str,
start: usize,
checkpoints: &mut ControlledScanCheckpoints<'_>,
) -> ParseControlResult<Option<usize>> {
for (offset, byte) in input.as_bytes()[start..].iter().copied().enumerate() {
checkpoints.scanned(1)?;
if matches!(byte, b'\r' | b'\n') {
return Ok(Some(start + offset));
}
}
Ok(None)
}
#[cfg(test)]
fn parse_directive_like_upstream(raw: &str) -> Result<Option<Directive>> {
let control = ParseControl::new();
parse_directive_like_upstream_controlled(raw, &control)
.expect("a private parse control cannot be cancelled")
}
fn parse_directive_like_upstream_controlled(
raw: &str,
control: &ParseControl,
) -> ParseControlResult<Result<Option<Directive>>> {
let normalized = normalize_directive_quotes_controlled(raw, control)?;
parse_directive_controlled(normalized.as_ref(), control)
}
fn normalize_directive_quotes_controlled<'a>(
raw: &'a str,
control: &ParseControl,
) -> ParseControlResult<Cow<'a, str>> {
let mut checkpoints = ControlledScanCheckpoints::new(control)?;
let mut first_quote = None;
for (index, byte) in raw.as_bytes().iter().copied().enumerate() {
checkpoints.scanned(1)?;
if byte == b'\'' {
first_quote = Some(index);
break;
}
}
let Some(first_quote) = first_quote else {
checkpoints.finish()?;
return Ok(Cow::Borrowed(raw));
};
let mut normalized = String::with_capacity(raw.len());
let mut cursor = 0usize;
for (relative, byte) in raw.as_bytes()[first_quote..].iter().copied().enumerate() {
checkpoints.scanned(1)?;
if byte != b'\'' {
continue;
}
let quote = first_quote + relative;
push_frontmatter_str_controlled(&mut normalized, &raw[cursor..quote], &mut checkpoints)?;
normalized.push('"');
cursor = quote + 1;
}
push_frontmatter_str_controlled(&mut normalized, &raw[cursor..], &mut checkpoints)?;
checkpoints.finish()?;
Ok(Cow::Owned(normalized))
}
#[cfg(test)]
fn detect_directives(input: &str) -> Result<Vec<Directive>> {
let mut directives = Vec::new();
for block in directive_blocks(input, DirectiveRecoveryMode::Strict) {
let Some(raw) = block.raw else {
continue;
};
if let Some(directive) = parse_directive_like_upstream(raw)? {
directives.push(directive);
}
}
Ok(directives)
}
#[derive(Clone)]
enum DirectiveValuePathSegment {
Key(String),
Index(usize),
}
#[derive(Clone, Copy)]
enum DirectiveDictionaryKind {
NodeColors,
IconReferences,
}
#[cfg(test)]
fn sanitize_directive(value: &mut Value) {
let control = ParseControl::new();
sanitize_directive_controlled(value, &control)
.expect("a private parse control cannot be cancelled");
}
fn sanitize_directive_controlled(
value: &mut Value,
control: &ParseControl,
) -> ParseControlResult<()> {
control.checkpoint()?;
let mut stack = vec![Vec::<DirectiveValuePathSegment>::new()];
let mut visited = 0usize;
while let Some(path) = stack.pop() {
if visited.is_multiple_of(64) {
control.checkpoint()?;
}
visited = visited.saturating_add(1);
let Some(current) = directive_value_at_path_mut(value, &path) else {
continue;
};
match current {
Value::Object(map) => {
if let Some(old) = map.remove("secure") {
crate::config::drop_value_nonrecursive(old);
}
let blocked_keys = map
.iter()
.filter(|(key, value)| {
is_suspicious_directive_key(key)
|| !crate::generated::is_default_config_key(key)
|| value.is_null()
})
.map(|(key, _)| key)
.cloned()
.collect::<Vec<_>>();
for key in blocked_keys {
if let Some(old) = map.remove(&key) {
crate::config::drop_value_nonrecursive(old);
}
}
let child_keys = map.keys().cloned().collect::<Vec<_>>();
for key in child_keys.into_iter().rev() {
if let Some(kind) = directive_dictionary_kind(&key)
&& map
.get_mut(&key)
.is_some_and(|child| sanitize_directive_dictionary(child, kind))
{
continue;
}
let mut child_path = path.clone();
child_path.push(DirectiveValuePathSegment::Key(key));
stack.push(child_path);
}
}
Value::Array(arr) => {
for idx in (0..arr.len()).rev() {
let mut child_path = path.clone();
child_path.push(DirectiveValuePathSegment::Index(idx));
stack.push(child_path);
}
}
Value::String(s) => {
if directive_path_is_css(&path) && !css_braces_are_balanced(s) {
*s = "{ /* ERROR: Unbalanced CSS */ }".to_string();
}
let blocked = s.contains('<')
|| s.contains('>')
|| s.contains("url(data:")
|| (directive_path_is_theme_variable(&path)
&& !theme_variable_value_is_allowed(s));
if blocked {
s.clear();
}
}
_ => {}
}
}
control.checkpoint()
}
fn directive_path_is_css(path: &[DirectiveValuePathSegment]) -> bool {
matches!(
path.last(),
Some(DirectiveValuePathSegment::Key(key))
if ["themeCSS", "fontFamily", "altFontFamily"]
.iter()
.any(|css_key| key.contains(css_key))
)
}
fn directive_path_is_theme_variable(path: &[DirectiveValuePathSegment]) -> bool {
matches!(
path.iter().rev().nth(1),
Some(DirectiveValuePathSegment::Key(key)) if key == "themeVariables"
)
}
fn theme_variable_value_is_allowed(value: &str) -> bool {
value.bytes().all(|byte| {
byte.is_ascii_digit()
|| byte.is_ascii_alphabetic()
|| matches!(
byte,
b' ' | b'"' | b'#' | b'%' | b'(' | b')' | b',' | b'.' | b';'
)
})
}
fn is_suspicious_directive_key(key: &str) -> bool {
key.starts_with("__") || key.contains("proto") || key.contains("constr")
}
fn css_braces_are_balanced(css: &str) -> bool {
let mut depth = 0usize;
for ch in css.chars() {
match ch {
'{' => depth += 1,
'}' => {
let Some(next) = depth.checked_sub(1) else {
return false;
};
depth = next;
}
_ => {}
}
}
depth == 0
}
fn directive_dictionary_kind(key: &str) -> Option<DirectiveDictionaryKind> {
match key {
"nodeColors" => Some(DirectiveDictionaryKind::NodeColors),
"filenameIcons" | "extensionIcons" => Some(DirectiveDictionaryKind::IconReferences),
_ => None,
}
}
fn sanitize_directive_dictionary(value: &mut Value, kind: DirectiveDictionaryKind) -> bool {
let is_valid_value = |value: &Value| {
value.as_str().is_some_and(|value| match kind {
DirectiveDictionaryKind::NodeColors => is_valid_node_color(value),
DirectiveDictionaryKind::IconReferences => is_valid_icon_reference(value),
})
};
match value {
Value::Object(map) => {
let blocked_keys = map
.iter()
.filter(|(key, value)| is_suspicious_dictionary_key(key) || !is_valid_value(value))
.map(|(key, _)| key.clone())
.collect::<Vec<_>>();
for key in blocked_keys {
if let Some(old) = map.remove(&key) {
crate::config::drop_value_nonrecursive(old);
}
}
true
}
Value::Array(values) => {
for value in values.iter_mut().filter(|value| !is_valid_value(value)) {
let old = std::mem::replace(value, Value::Null);
crate::config::drop_value_nonrecursive(old);
}
true
}
_ => false,
}
}
fn is_suspicious_dictionary_key(key: &str) -> bool {
is_suspicious_directive_key(key)
}
fn is_valid_icon_reference(value: &str) -> bool {
let mut segments = value.split(':');
let Some(first) = segments.next() else {
return false;
};
let second = segments.next();
is_valid_icon_reference_segment(first)
&& second.is_none_or(is_valid_icon_reference_segment)
&& segments.next().is_none()
}
fn is_valid_icon_reference_segment(segment: &str) -> bool {
!segment.is_empty()
&& segment
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
}
fn is_valid_node_color(value: &str) -> bool {
if let Some(hex) = value.strip_prefix('#') {
return (3..=8).contains(&hex.len()) && hex.bytes().all(|byte| byte.is_ascii_hexdigit());
}
if is_valid_node_color_function(value, "rgb(") || is_valid_node_color_function(value, "hsl(") {
return true;
}
!value.is_empty() && value.bytes().all(|byte| byte.is_ascii_alphabetic())
}
fn is_valid_node_color_function(value: &str, prefix: &str) -> bool {
let Some(actual_prefix) = value.get(..prefix.len()) else {
return false;
};
if !actual_prefix.eq_ignore_ascii_case(prefix) || !value.ends_with(')') {
return false;
}
let inner = &value[prefix.len()..value.len() - 1];
!inner.is_empty()
&& inner.chars().all(|ch| {
ch.is_ascii_digit() || is_js_regex_whitespace(ch) || matches!(ch, '%' | ',' | '.')
})
}
fn is_js_regex_whitespace(ch: char) -> bool {
if ('\u{2000}'..='\u{200A}').contains(&ch) {
return true;
}
matches!(
ch,
'\u{0009}'
| '\u{000A}'
| '\u{000B}'
| '\u{000C}'
| '\u{000D}'
| '\u{0020}'
| '\u{00A0}'
| '\u{1680}'
| '\u{2028}'
| '\u{2029}'
| '\u{202F}'
| '\u{205F}'
| '\u{3000}'
| '\u{FEFF}'
)
}
fn directive_value_at_path_mut<'a>(
mut value: &'a mut Value,
path: &[DirectiveValuePathSegment],
) -> Option<&'a mut Value> {
for segment in path {
match segment {
DirectiveValuePathSegment::Key(key) => {
value = value.as_object_mut()?.get_mut(key)?;
}
DirectiveValuePathSegment::Index(idx) => {
value = value.as_array_mut()?.get_mut(*idx)?;
}
}
}
Some(value)
}
pub(crate) fn directive_removal_ranges_controlled(
text: &str,
control: &ParseControl,
) -> ParseControlResult<Vec<std::ops::Range<usize>>> {
Ok(
directive_blocks_controlled(text, DirectiveRecoveryMode::Strict, control)?
.into_iter()
.map(|block| block.range)
.collect(),
)
}
fn parse_directive_controlled(
raw: &str,
control: &ParseControl,
) -> ParseControlResult<Result<Option<Directive>>> {
let mut checkpoints = ControlledScanCheckpoints::new(control)?;
let (raw_start, raw_end) = trim_whitespace_bounds_controlled(raw, &mut checkpoints)?;
let raw = &raw[raw_start..raw_end];
if raw.is_empty() {
checkpoints.finish()?;
return Ok(Ok(None));
}
let mut type_end = 0usize;
for (index, ch) in raw.char_indices() {
checkpoints.scanned(ch.len_utf8())?;
if !ch.is_ascii_alphanumeric() && ch != '_' {
break;
}
type_end = index + ch.len_utf8();
}
if type_end == 0 {
checkpoints.finish()?;
return Ok(Ok(None));
}
let mut ty = String::with_capacity(type_end);
push_frontmatter_str_controlled(&mut ty, &raw[..type_end], &mut checkpoints)?;
let whitespace = trim_start_whitespace_controlled(&raw[type_end..], &mut checkpoints)?;
let mut position = type_end + whitespace;
let args = if raw.as_bytes().get(position) == Some(&b':') {
checkpoints.scanned(1)?;
position += 1;
let whitespace = trim_start_whitespace_controlled(&raw[position..], &mut checkpoints)?;
position += whitespace;
let rest = &raw[position..];
let (rest_start, rest_end) = trim_whitespace_bounds_controlled(rest, &mut checkpoints)?;
let rest = &rest[rest_start..rest_end];
if rest.is_empty() {
None
} else if rest.starts_with('{') || rest.starts_with('[') {
if rest.len() > MAX_DIRECTIVE_CONFIG_PARSE_BYTES {
return Ok(Err(Error::InvalidDirectiveJson {
message: format!(
"directive config exceeds the safe parser budget of {MAX_DIRECTIVE_CONFIG_PARSE_BYTES} bytes"
),
}));
}
if config_nesting_exceeds_limit_controlled(rest, control)? {
return Ok(Err(Error::InvalidDirectiveJson {
message: format!("config nesting exceeds {MAX_CONFIG_NESTING_DEPTH} levels"),
}));
}
checkpoints.finish()?;
parse_directive_config_value_controlled(rest, control)?
} else {
let mut value = String::with_capacity(rest.len());
push_frontmatter_str_controlled(&mut value, rest, &mut checkpoints)?;
Some(Value::String(value))
}
} else {
None
};
checkpoints.finish()?;
Ok(Ok(Some(Directive { ty, args })))
}
fn parse_directive_config_value_controlled(
input: &str,
control: &ParseControl,
) -> ParseControlResult<Option<Value>> {
control.checkpoint()?;
let parsed = json5::from_str::<Value>(input).ok();
if let Err(cancelled) = control.checkpoint() {
if let Some(value) = parsed {
crate::config::drop_value_nonrecursive(value);
}
return Err(cancelled);
}
Ok(parsed)
}
#[cfg(test)]
fn config_nesting_exceeds_limit(text: &str) -> bool {
let control = ParseControl::new();
config_nesting_exceeds_limit_controlled(text, &control)
.expect("a private parse control cannot be cancelled")
}
fn config_nesting_exceeds_limit_controlled(
text: &str,
control: &ParseControl,
) -> ParseControlResult<bool> {
let mut checkpoints = ControlledScanCheckpoints::new(control)?;
if max_flow_collection_depth_controlled(text, &mut checkpoints)? > MAX_CONFIG_NESTING_DEPTH {
checkpoints.finish()?;
return Ok(true);
}
let exceeds =
max_yaml_indent_depth_controlled(text, &mut checkpoints)? > MAX_CONFIG_NESTING_DEPTH;
checkpoints.finish()?;
Ok(exceeds)
}
fn max_flow_collection_depth_controlled(
text: &str,
checkpoints: &mut ControlledScanCheckpoints<'_>,
) -> ParseControlResult<usize> {
let mut max_depth = 0usize;
let mut depth = 0usize;
let mut quote = None;
let mut escaped = false;
for ch in text.chars() {
checkpoints.scanned(ch.len_utf8())?;
if let Some(q) = quote {
if escaped {
escaped = false;
continue;
}
if ch == '\\' {
escaped = true;
continue;
}
if ch == q {
quote = None;
}
continue;
}
match ch {
'"' | '\'' => quote = Some(ch),
'{' | '[' => {
depth = depth.saturating_add(1);
max_depth = max_depth.max(depth);
}
'}' | ']' => {
depth = depth.saturating_sub(1);
}
_ => {}
}
}
Ok(max_depth)
}
fn max_yaml_indent_depth_controlled(
text: &str,
checkpoints: &mut ControlledScanCheckpoints<'_>,
) -> ParseControlResult<usize> {
let mut indents = Vec::<usize>::new();
let mut max_depth = 0usize;
let mut line_start = 0usize;
while line_start < text.len() {
let newline = find_newline_controlled(text, line_start, checkpoints)?;
let line_end = newline.unwrap_or(text.len());
let line_end =
if newline.is_some() && line_end > line_start && text.as_bytes()[line_end - 1] == b'\r'
{
checkpoints.scanned(1)?;
line_end - 1
} else {
line_end
};
let line = &text[line_start..line_end];
let (trimmed_start, trimmed_end) = trim_whitespace_bounds_controlled(line, checkpoints)?;
let trimmed = &line[trimmed_start..trimmed_end];
if !trimmed.is_empty() && !trimmed.starts_with('#') {
let indent = leading_ascii_space_count_controlled(line, checkpoints)?;
while indents.last().is_some_and(|prev| indent <= *prev) {
checkpoints.scanned(1)?;
indents.pop();
}
indents.push(indent);
let inline_sequence_depth =
yaml_inline_sequence_indicator_count_controlled(trimmed, checkpoints)?;
max_depth = max_depth.max(indents.len() + inline_sequence_depth.saturating_sub(1));
}
let Some(newline) = newline else {
break;
};
line_start = newline + 1;
}
Ok(max_depth)
}
fn trim_whitespace_bounds_controlled(
text: &str,
checkpoints: &mut ControlledScanCheckpoints<'_>,
) -> ParseControlResult<(usize, usize)> {
let mut start = text.len();
for (idx, ch) in text.char_indices() {
checkpoints.scanned(ch.len_utf8())?;
if !ch.is_whitespace() {
start = idx;
break;
}
}
if start == text.len() {
return Ok((text.len(), text.len()));
}
let mut end = start;
for (idx, ch) in text.char_indices().rev() {
checkpoints.scanned(ch.len_utf8())?;
if !ch.is_whitespace() {
end = idx + ch.len_utf8();
break;
}
}
Ok((start, end))
}
fn leading_ascii_space_count_controlled(
text: &str,
checkpoints: &mut ControlledScanCheckpoints<'_>,
) -> ParseControlResult<usize> {
let mut count = 0usize;
for byte in text.bytes() {
checkpoints.scanned(1)?;
if byte != b' ' {
break;
}
count += 1;
}
Ok(count)
}
fn trim_start_whitespace_controlled(
text: &str,
checkpoints: &mut ControlledScanCheckpoints<'_>,
) -> ParseControlResult<usize> {
let mut start = text.len();
for (idx, ch) in text.char_indices() {
checkpoints.scanned(ch.len_utf8())?;
if !ch.is_whitespace() {
start = idx;
break;
}
}
Ok(start)
}
fn yaml_inline_sequence_indicator_count_controlled(
mut text: &str,
checkpoints: &mut ControlledScanCheckpoints<'_>,
) -> ParseControlResult<usize> {
let mut count = 0usize;
loop {
let Some(after_dash) = text.strip_prefix('-') else {
return Ok(count);
};
checkpoints.scanned(1)?;
if let Some(ch) = after_dash.chars().next() {
checkpoints.scanned(ch.len_utf8())?;
if !ch.is_whitespace() {
return Ok(count);
}
}
count += 1;
let trimmed_start = trim_start_whitespace_controlled(after_dash, checkpoints)?;
text = &after_dash[trimmed_start..];
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::{Map, json};
use std::sync::atomic::{AtomicUsize, Ordering};
static INIT_DETECTOR_CALLS: AtomicUsize = AtomicUsize::new(0);
fn counting_flowchart_detector(_text: &str, _config: &mut MermaidConfig) -> bool {
INIT_DETECTOR_CALLS.fetch_add(1, Ordering::Relaxed);
true
}
fn transformed(input: &str, transform: fn(&mut PreprocessedSource)) -> String {
let mut source = PreprocessedSource::new(input);
transform(&mut source);
source.into_text()
}
#[test]
fn controlled_frontmatter_apis_preserve_existing_block_semantics() {
for (source, expected_block) in [
("---\ntitle: Demo\n---\nflowchart TD\n", true),
(" \t--- \r\n \ttitle: Demo\r\n \t---\r\nflowchart TD", true),
(
"\u{2003}---\n\u{2003}title: Demo\n\u{2003}---\nflowchart TD",
true,
),
("---\r\r\ntitle: Demo\r\n---\r\r\nflowchart TD", true),
("---\ntitle: Demo\n--- \u{2003}\nflowchart TD", true),
("---\ntitle: Demo\n---", true),
("--- trailing\ntitle: Demo\n---\nflowchart TD", false),
("---\ntitle: Demo\n \t---\nflowchart TD", false),
("---", false),
] {
let control = ParseControl::new();
let controlled = split_frontmatter_block_controlled(source, &control)
.expect("an active parse control must not cancel");
let wrapped = split_frontmatter_block(source);
assert_eq!(controlled.is_some(), expected_block, "source: {source:?}");
assert_eq!(
controlled.is_some(),
wrapped.is_some(),
"source: {source:?}"
);
if let (Some(controlled), Some(wrapped)) = (controlled, wrapped) {
assert_eq!(controlled.full, wrapped.full, "source: {source:?}");
assert_eq!(controlled.body, wrapped.body, "source: {source:?}");
assert_eq!(controlled.indent, wrapped.indent, "source: {source:?}");
assert_eq!(
controlled.dedented_body, wrapped.dedented_body,
"source: {source:?}"
);
assert_eq!(controlled.stripped, wrapped.stripped, "source: {source:?}");
let location = locate_frontmatter_block_controlled(source, &ParseControl::new())
.expect("an active parse control must not cancel")
.expect("a split block must have a location");
assert_eq!(location.full, controlled.full, "source: {source:?}");
assert_eq!(location.body, controlled.body, "source: {source:?}");
assert_eq!(location.indent, controlled.indent, "source: {source:?}");
assert_eq!(location.stripped, controlled.stripped, "source: {source:?}");
assert_eq!(&source[location.full.end..], controlled.stripped);
}
}
}
#[test]
fn controlled_frontmatter_split_preserves_indented_body_bytes() {
let source =
" ---\r\n title: Demo\r\n config:\r\n theme: dark\r\n ---\r\nflowchart TD";
let block = split_frontmatter_block_controlled(source, &ParseControl::new())
.expect("an active parse control must not cancel")
.expect("frontmatter block");
assert_eq!(block.indent, " ");
assert_eq!(
&source[block.body.start..block.body.end],
" title: Demo\r\n config:\r\n theme: dark\r"
);
assert_eq!(
block.dedented_body,
"title: Demo\r\nconfig:\r\n theme: dark\r"
);
assert_eq!(block.stripped, "flowchart TD");
}
#[test]
fn controlled_frontmatter_split_preserves_long_opening_body_and_indent_semantics() {
let indent = " ".repeat(8 * 1024);
let value = "x".repeat(8 * 1024);
let indented_body = format!("{indent}title: {value}");
let source = format!("{indent}---\n{indented_body}\n{indent}---\nflowchart TD");
let block = split_frontmatter_block_controlled(&source, &ParseControl::new())
.expect("an active parse control must not cancel")
.expect("frontmatter block");
let dedented_body = format!("title: {value}");
assert_eq!(block.indent, indent.as_str());
assert_eq!(
&source[block.body.start..block.body.end],
indented_body.as_str()
);
assert_eq!(block.dedented_body.as_ref(), dedented_body.as_str());
assert_eq!(block.stripped, "flowchart TD");
}
#[test]
fn frontmatter_location_cancels_deterministically_on_a_long_opening_line() {
let indent = " ".repeat(16 * 1024);
let source = format!("{indent}---\n{indent}---\nflowchart TD");
let control = ParseControl::new();
control.cancel_after_checkpoints(2);
assert!(locate_frontmatter_block_controlled(&source, &control).is_err());
}
#[test]
fn frontmatter_location_cancels_deterministically_on_a_long_body() {
let source = format!("---\n{}\n---\nflowchart TD", "x".repeat(16 * 1024));
let control = ParseControl::new();
control.cancel_after_checkpoints(2);
assert!(locate_frontmatter_block_controlled(&source, &control).is_err());
}
#[test]
fn frontmatter_location_cancels_deterministically_on_a_long_closing_line() {
let source = format!(
"---\ntitle: Demo\n---{}\nflowchart TD",
" ".repeat(16 * 1024)
);
let control = ParseControl::new();
control.cancel_after_checkpoints(2);
assert!(locate_frontmatter_block_controlled(&source, &control).is_err());
}
#[test]
fn frontmatter_dedent_cancels_deterministically_on_a_long_indented_body() {
let body = format!(" {}", "x".repeat(16 * 1024));
let control = ParseControl::new();
control.cancel_after_checkpoints(2);
assert!(dedent_frontmatter_body_controlled(&body, " ", &control).is_err());
}
#[test]
fn controlled_frontmatter_yaml_fields_match_the_legacy_wrapper() {
let yaml = "title: Demo\nconfig:\n theme: dark\n";
let controlled = parse_frontmatter_yaml_fields_controlled(yaml, &ParseControl::new())
.expect("an active parse control must not cancel")
.expect("valid frontmatter YAML");
assert_eq!(controlled, parse_frontmatter_yaml_fields(yaml).unwrap());
let cancelled = ParseControl::new();
cancelled.cancel();
assert!(parse_frontmatter_yaml_fields_controlled(yaml, &cancelled).is_err());
}
#[test]
fn bounded_frontmatter_yaml_fields_honor_caller_materialization_limits() {
let result = parse_frontmatter_yaml_fields_bounded_controlled(
"values: [one, two, three, four]\n",
1024,
16,
8,
&ParseControl::new(),
)
.expect("an active parse control must not cancel");
let error = result.expect_err("the caller budget must reject materialization");
assert!(error.contains("safe materialization budget"));
}
#[test]
fn normalize_crlf_matches_mermaid_line_ending_cleanup() {
assert_eq!(
transformed("flowchart TD\r\nA-->B\rC-->D\n", normalize_crlf),
"flowchart TD\nA-->B\nC-->D\n"
);
assert_eq!(transformed("\r\r\n\n", normalize_crlf), "\n\n\n");
}
#[test]
fn normalize_html_tag_attributes_matches_mermaid_cleanup_shape() {
assert_eq!(
transformed(
r#"<span title="A" data-empty="">Label</span><br disabled="yes">"#,
normalize_html_tag_attributes_like_upstream,
),
r#"<span title='A' data-empty=''>Label</span><br disabled='yes'>"#
);
assert_eq!(
transformed(
r#"<é title="A"><_x value="B"><1 n="C">"#,
normalize_html_tag_attributes_like_upstream,
),
r#"<é title="A"><_x value='B'><1 n='C'>"#
);
assert_eq!(
transformed(
r#"<span a="x" title="A>B">"#,
normalize_html_tag_attributes_like_upstream,
),
r#"<span a='x' title="A>B">"#
);
assert_eq!(
transformed(
r#"<<span title="A">"#,
normalize_html_tag_attributes_like_upstream,
),
r#"<<span title='A'>"#
);
}
#[test]
fn normalize_html_attribute_quotes_keep_exact_unicode_source_spans() {
let original = r#"flowchart TD
A["<span title="😀">Label</span>"]
"#;
let mut source = PreprocessedSource::new(original);
normalize_html_tag_attributes_like_upstream(&mut source);
assert!(source.text().contains("title='😀'"));
let emoji = source.text().find('😀').unwrap();
let mapped = source
.try_map_span(crate::SourceSpan::new(emoji, emoji + '😀'.len_utf8()))
.expect("normalized attribute value span");
assert_eq!(&original[mapped.start..mapped.end], "😀");
}
#[test]
fn multiple_init_config_directives_detect_the_diagram_only_once() {
INIT_DETECTOR_CALLS.store(0, Ordering::Relaxed);
let input = concat!(
"%%{init: {\"config\": {\"curve\": \"linear\"}}}%%\n",
"%%{initialize: {\"config\": {\"htmlLabels\": false}}}%%\n",
"flowchart TD\nA-->B\n",
);
let directives = detect_directives(input).expect("directives parse");
let mut registry = DetectorRegistry::new();
registry.add_fn("flowchart-v2", counting_flowchart_detector);
let config = detect_init(&directives, input, ®istry, None).expect("init merges");
assert_eq!(INIT_DETECTOR_CALLS.load(Ordering::Relaxed), 1);
assert_eq!(
config
.as_value()
.pointer("/flowchart/curve")
.and_then(Value::as_str),
Some("linear")
);
assert_eq!(
config
.as_value()
.pointer("/flowchart/htmlLabels")
.and_then(Value::as_bool),
Some(false)
);
}
#[test]
fn encode_entity_placeholders_matches_mermaid_ascii_word_shape() {
assert_eq!(
transformed(
"Hello #there; #andHere;#77653;",
encode_mermaid_entities_like_upstream,
),
"Hello fl°there¶ß fl°andHere¶ßfl°°77653¶ß"
);
assert_eq!(
transformed(
"style this; is ; everything :something#not-nothing; and this too;",
encode_mermaid_entities_like_upstream,
),
"style this; is ; everything :something#not-nothing; and this too"
);
assert_eq!(
transformed(
"classDef this; is ; everything :something#not-nothing; and this too;",
encode_mermaid_entities_like_upstream,
),
"classDef this; is ; everything :something#not-nothing; and this too"
);
assert_eq!(
transformed(
"style a fill:#fff; style b fill:#000;",
encode_mermaid_entities_like_upstream,
),
"style a fill:fl°fff¶ß style b fill:#000"
);
assert_eq!(
transformed("style a fill: #fff;", encode_mermaid_entities_like_upstream,),
"style a fill: fl°fff¶ß"
);
assert_eq!(
transformed(
"#é; #+123; #has-dash;",
encode_mermaid_entities_like_upstream,
),
"#é; #+123; #has-dash;"
);
}
#[test]
fn sanitize_directive_handles_deep_values_with_small_stack() {
const DEPTH: usize = 2_048;
let mut value = deep_directive_value(DEPTH, Value::String("<blocked>".to_string()));
let handle = std::thread::Builder::new()
.name("preprocess-deep-directive-sanitize".to_string())
.stack_size(64 * 1024)
.spawn(move || {
sanitize_directive(&mut value);
assert_eq!(
deep_directive_leaf(&value, DEPTH).and_then(Value::as_str),
Some("")
);
crate::config::drop_value_nonrecursive(value);
})
.expect("spawn deep directive sanitizer test");
handle
.join()
.expect("deep directive sanitizer should finish without stack overflow");
}
#[test]
fn sanitize_directive_replaces_unbalanced_css_like_mermaid() {
let mut value = json!({
"themeCSS": "} * { background: red }",
"flowchart": {
"fontFamily": "valid { nested: value; }",
"altFontFamily": "missing { close"
}
});
sanitize_directive(&mut value);
assert_eq!(
value["themeCSS"],
Value::String("{ /* ERROR: Unbalanced CSS */ }".to_string())
);
assert_eq!(value["flowchart"]["fontFamily"], "valid { nested: value; }");
assert!(value["flowchart"].get("altFontFamily").is_none());
}
#[test]
fn sanitize_directive_uses_mermaid_theme_variable_allowlist() {
let mut value = json!({
"themeVariables": {
"primaryColor": "#123456",
"secondaryColor": "rgb(1, 2, 3)",
"tertiaryColor": "url(javascript:alert(1))",
"noteBkgColor": "hsl(120, 50%, 25.5%)",
"noteTextColor": "red-blue"
}
});
sanitize_directive(&mut value);
assert_eq!(value["themeVariables"]["primaryColor"], "#123456");
assert_eq!(value["themeVariables"]["secondaryColor"], "rgb(1, 2, 3)");
assert_eq!(
value["themeVariables"]["tertiaryColor"],
Value::String(String::new())
);
assert_eq!(
value["themeVariables"]["noteBkgColor"],
"hsl(120, 50%, 25.5%)"
);
assert_eq!(
value["themeVariables"]["noteTextColor"],
Value::String(String::new())
);
}
#[test]
fn sanitize_directive_uses_generated_config_shape_for_all_value_kinds() {
let mut value = json!({
"notAConfigKey": "removed",
"theme": null,
"prototype": "removed",
"constructor": "removed",
"deterministicIDSeed": "accepted undefined key",
"sequence": {
"messageFont": "accepted function key",
"unknownNestedKey": true
},
"secure": ["theme"],
"flowchart": {
"secure": ["htmlLabels"],
"htmlLabels": false
}
});
sanitize_directive(&mut value);
assert_eq!(
value,
json!({
"deterministicIDSeed": "accepted undefined key",
"sequence": {
"messageFont": "accepted function key"
},
"flowchart": {
"htmlLabels": false
}
})
);
}
#[test]
fn sanitize_directive_preserves_valid_dictionary_entries() {
let mut value = json!({
"sankey": {
"nodeColors": {
"shortHex": "#abc",
"alphaHex": "#12345678",
"rgb": "rgb(0, 10%, 255)",
"hsl": "hsl(120, 50%, 25.5%)",
"named": "rebeccapurple"
}
},
"treeView": {
"filenameIcons": {
"Makefile": "cmake",
"README.md": "fa:bell"
},
"extensionIcons": {
".ts": "logos:typescript-icon",
".txt": "none"
}
}
});
sanitize_directive(&mut value);
assert_eq!(value["sankey"]["nodeColors"]["shortHex"], "#abc");
assert_eq!(value["sankey"]["nodeColors"]["alphaHex"], "#12345678");
assert_eq!(value["sankey"]["nodeColors"]["rgb"], "rgb(0, 10%, 255)");
assert_eq!(value["sankey"]["nodeColors"]["hsl"], "hsl(120, 50%, 25.5%)");
assert_eq!(value["sankey"]["nodeColors"]["named"], "rebeccapurple");
assert_eq!(value["treeView"]["filenameIcons"]["README.md"], "fa:bell");
assert_eq!(
value["treeView"]["extensionIcons"][".ts"],
"logos:typescript-icon"
);
}
#[test]
fn sanitize_directive_removes_invalid_dictionary_values() {
let mut value = json!({
"sankey": {
"nodeColors": {
"valid": "#ff0000",
"short": "#12",
"function": "url(javascript:alert(1))",
"wrongType": 42
}
},
"treeView": {
"filenameIcons": {
"valid": "docker",
"markup": "<script>alert(1)</script>",
"wrongType": false
},
"extensionIcons": {
".ts": "logos:typescript-icon",
".css": "not a valid name",
".json": "one:two:three"
}
}
});
sanitize_directive(&mut value);
assert_eq!(value["sankey"]["nodeColors"], json!({ "valid": "#ff0000" }));
assert_eq!(
value["treeView"]["filenameIcons"],
json!({ "valid": "docker" })
);
assert_eq!(
value["treeView"]["extensionIcons"],
json!({ ".ts": "logos:typescript-icon" })
);
}
#[test]
fn sanitize_directive_removes_suspicious_dictionary_keys() {
let mut value = json!({
"sankey": {
"nodeColors": {
"__proto__hack": "red",
"prototype": "green",
"constructor.js": "blue",
"safe": "black"
}
},
"treeView": {
"filenameIcons": {
"__proto__hack": "docker",
"prototype.ts": "docker",
"constructor.js": "docker",
"a.ts": "docker"
}
}
});
sanitize_directive(&mut value);
assert_eq!(value["sankey"]["nodeColors"], json!({ "safe": "black" }));
assert_eq!(
value["treeView"]["filenameIcons"],
json!({ "a.ts": "docker" })
);
}
#[test]
fn sanitize_directive_validates_dictionary_arrays_like_javascript_objects() {
let mut value = json!({
"sankey": {
"nodeColors": ["#ff0000", "url(javascript:alert(1))", 42]
},
"treeView": {
"extensionIcons": ["logos:typescript-icon", "not a valid name", false]
}
});
sanitize_directive(&mut value);
assert_eq!(
value["sankey"]["nodeColors"],
json!(["#ff0000", null, null])
);
assert_eq!(
value["treeView"]["extensionIcons"],
json!(["logos:typescript-icon", null, null])
);
}
#[test]
fn config_nesting_counts_inline_yaml_sequence_indicators() {
let yaml = format!(
"config:\n {}\"leaf\"",
"- ".repeat(MAX_CONFIG_NESTING_DEPTH + 1)
);
assert!(config_nesting_exceeds_limit(&yaml));
}
#[test]
fn controlled_config_nesting_preserves_existing_depth_semantics() {
let deep_flow = "[".repeat(MAX_CONFIG_NESTING_DEPTH + 1);
for (yaml, expected) in [
("title: '[not, nesting]'\n", false),
("config:\r\n theme: dark\r\n", false),
("# [ignored]\nconfig: { theme: dark }\n", false),
(deep_flow.as_str(), true),
] {
let controlled = config_nesting_exceeds_limit_controlled(yaml, &ParseControl::new())
.expect("an active parse control must not cancel");
assert_eq!(controlled, expected, "yaml: {yaml:?}");
assert_eq!(controlled, config_nesting_exceeds_limit(yaml));
}
}
#[test]
fn config_nesting_flow_scan_cancels_deterministically_on_a_long_line() {
let yaml = format!("title: \"{}\"", "x".repeat(16 * 1024));
let control = ParseControl::new();
control.cancel_after_checkpoints(2);
assert!(config_nesting_exceeds_limit_controlled(&yaml, &control).is_err());
}
#[test]
fn config_nesting_indent_scan_cancels_deterministically_on_a_long_line() {
let yaml = format!("{}key: value", " ".repeat(16 * 1024));
let control = ParseControl::new();
control.cancel_after_checkpoints(2);
let mut checkpoints = ControlledScanCheckpoints::new(&control).unwrap();
assert!(max_yaml_indent_depth_controlled(&yaml, &mut checkpoints).is_err());
}
#[test]
fn config_nesting_trim_cancels_deterministically_on_long_whitespace() {
let line = format!("{}value{}", " ".repeat(16 * 1024), " ".repeat(16 * 1024));
let control = ParseControl::new();
control.cancel_after_checkpoints(2);
let mut checkpoints = ControlledScanCheckpoints::new(&control).unwrap();
assert!(trim_whitespace_bounds_controlled(&line, &mut checkpoints).is_err());
}
fn deep_directive_value(depth: usize, leaf: Value) -> Value {
let mut value = leaf;
for _ in 0..depth {
let mut map = Map::new();
map.insert("flowchart".to_string(), value);
value = Value::Object(map);
}
value
}
fn deep_directive_leaf(mut value: &Value, depth: usize) -> Option<&Value> {
for _ in 0..depth {
value = value.as_object()?.get("flowchart")?;
}
Some(value)
}
}