#[must_use]
pub fn escape_html(s: &str) -> String {
let mut result = String::with_capacity(s.len());
for c in s.chars() {
match c {
'&' => result.push_str("&"),
'<' => result.push_str("<"),
'>' => result.push_str(">"),
'"' => result.push_str("""),
'\'' => result.push_str("'"),
_ => result.push(c),
}
}
result
}
#[must_use]
pub fn escape_html_attr(s: &str) -> String {
let mut result = String::with_capacity(s.len());
for c in s.chars() {
match c {
'&' => result.push_str("&"),
'<' => result.push_str("<"),
'>' => result.push_str(">"),
'"' => result.push_str("""),
'\'' => result.push_str("'"),
'\n' => result.push_str(" "),
'\r' => result.push_str(" "),
'\t' => result.push_str("	"),
_ => result.push(c),
}
}
result
}
#[must_use]
pub fn escape_markdown_table(s: &str) -> String {
let mut result = String::with_capacity(s.len());
for c in s.chars() {
match c {
'|' => result.push_str("\\|"),
'\n' => result.push(' '),
'\r' => {}
'`' => result.push_str("\\`"),
'[' => result.push_str("\\["),
']' => result.push_str("\\]"),
_ => result.push(c),
}
}
result
}
#[must_use]
pub fn escape_markdown_inline(s: &str) -> String {
let mut result = String::with_capacity(s.len());
for c in s.chars() {
match c {
'*' => result.push_str("\\*"),
'_' => result.push_str("\\_"),
'`' => result.push_str("\\`"),
'[' => result.push_str("\\["),
']' => result.push_str("\\]"),
'#' => result.push_str("\\#"),
'!' => result.push_str("\\!"),
'~' => result.push_str("\\~"),
'|' => result.push_str("\\|"),
'<' => result.push_str("\\<"),
'>' => result.push_str("\\>"),
'\n' => result.push(' '),
'\r' => {}
_ => result.push(c),
}
}
result
}
#[must_use]
pub fn escape_markdown_list(s: &str) -> String {
let mut result = String::with_capacity(s.len());
for c in s.chars() {
match c {
'*' => result.push_str("\\*"),
'`' => result.push_str("\\`"),
'[' => result.push_str("\\["),
']' => result.push_str("\\]"),
'<' => result.push_str("\\<"),
'>' => result.push_str("\\>"),
'\n' => result.push_str("; "),
'\r' => {}
_ => result.push(c),
}
}
result
}
pub fn escape_html_opt(s: Option<&str>) -> String {
s.map_or_else(|| "-".to_string(), escape_html)
}
pub fn escape_md_opt(s: Option<&str>) -> String {
s.map_or_else(|| "-".to_string(), escape_markdown_table)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_escape_html_basic() {
assert_eq!(escape_html("hello"), "hello");
assert_eq!(escape_html("a & b"), "a & b");
assert_eq!(escape_html("a < b > c"), "a < b > c");
assert_eq!(escape_html("\"quoted\""), ""quoted"");
assert_eq!(escape_html("it's"), "it's");
}
#[test]
fn test_escape_html_xss_vectors() {
assert_eq!(
escape_html("<script>alert('xss')</script>"),
"<script>alert('xss')</script>"
);
assert_eq!(
escape_html("<img onerror=\"alert('xss')\">"),
"<img onerror="alert('xss')">"
);
assert_eq!(escape_html("<script>"), "&lt;script&gt;");
assert_eq!(
escape_html("<a href=\"javascript:alert('xss')\">click</a>"),
"<a href="javascript:alert('xss')">click</a>"
);
}
#[test]
fn test_escape_html_attr() {
assert_eq!(escape_html_attr("normal"), "normal");
assert_eq!(escape_html_attr("line1\nline2"), "line1 line2");
assert_eq!(escape_html_attr("with\ttab"), "with	tab");
}
#[test]
fn test_escape_markdown_table_basic() {
assert_eq!(escape_markdown_table("hello"), "hello");
assert_eq!(escape_markdown_table("a | b"), "a \\| b");
assert_eq!(escape_markdown_table("line1\nline2"), "line1 line2");
assert_eq!(escape_markdown_table("`code`"), "\\`code\\`");
}
#[test]
fn test_escape_markdown_table_malicious() {
assert_eq!(
escape_markdown_table("name|version|evil"),
"name\\|version\\|evil"
);
assert_eq!(
escape_markdown_table("row1\n| new | row |"),
"row1 \\| new \\| row \\|"
);
assert_eq!(
escape_markdown_table("[evil](http://malware.com)"),
"\\[evil\\](http://malware.com)"
);
assert_eq!(
escape_markdown_table("```\ncode block\n```"),
"\\`\\`\\` code block \\`\\`\\`"
);
}
#[test]
fn test_escape_markdown_inline() {
assert_eq!(escape_markdown_inline("hello"), "hello");
assert_eq!(escape_markdown_inline("**bold**"), "\\*\\*bold\\*\\*");
assert_eq!(escape_markdown_inline("_italic_"), "\\_italic\\_");
assert_eq!(escape_markdown_inline("[link](url)"), "\\[link\\](url)");
assert_eq!(escape_markdown_inline("# heading"), "\\# heading");
}
#[test]
fn test_escape_markdown_list() {
assert_eq!(escape_markdown_list("item"), "item");
assert_eq!(escape_markdown_list("multi\nline"), "multi; line");
assert_eq!(escape_markdown_list("[link]"), "\\[link\\]");
}
#[test]
fn test_escape_helpers() {
assert_eq!(escape_html_opt(Some("<test>")), "<test>");
assert_eq!(escape_html_opt(None), "-");
assert_eq!(escape_md_opt(Some("a | b")), "a \\| b");
assert_eq!(escape_md_opt(None), "-");
}
#[test]
fn test_empty_string() {
assert_eq!(escape_html(""), "");
assert_eq!(escape_markdown_table(""), "");
assert_eq!(escape_markdown_inline(""), "");
}
#[test]
fn test_unicode_preservation() {
assert_eq!(escape_html("日本語"), "日本語");
assert_eq!(escape_markdown_table("émoji 🎉"), "émoji 🎉");
assert_eq!(escape_html("Ω ≈ ∞"), "Ω ≈ ∞");
}
#[test]
fn test_realistic_sbom_data() {
assert_eq!(escape_html("lodash@4.17.21"), "lodash@4.17.21");
assert_eq!(escape_markdown_table("@types/node"), "@types/node");
assert_eq!(escape_html(">=1.0.0 <2.0.0"), ">=1.0.0 <2.0.0");
assert_eq!(
escape_html("pkg:npm/%40scope/name@1.0.0"),
"pkg:npm/%40scope/name@1.0.0"
);
}
}