use super::{escape_java_comment_text, escape_java_string_literal};
const RAW_UNICODE_ESCAPE: &str = concat!("\\", "u0041");
const ESCAPED_UNICODE_ESCAPE: &str = concat!("\\\\", "u0041");
const HOSTILE_NAMES: &[&str] = &[
r#"quote"inside"#,
r"back\slash",
RAW_UNICODE_ESCAPE,
r"\u{1F600}",
r"trailing\",
"new\nline",
"carriage\rreturn",
"tab\there",
"nul\0byte",
"bell\u{7}here",
"escape\u{1b}[0m",
"delete\u{7f}here",
"caf\u{e9}",
"emoji\u{1F600}name",
"line\u{2028}separator",
r#"breakout") String x; //"#,
"comment*/close",
"angle<bracket>and&",
];
#[test]
fn escapes_the_double_quote_that_would_close_the_literal() {
assert_eq!(escape_java_string_literal(r#"quote"inside"#), r#"quote\"inside"#);
}
#[test]
fn escapes_the_backslash_that_would_consume_the_next_character() {
assert_eq!(escape_java_string_literal(r"back\slash"), r"back\\slash");
assert_eq!(escape_java_string_literal(r"trailing\"), r"trailing\\");
}
#[test]
fn neutralizes_a_raw_unicode_escape_sequence() {
assert_eq!(escape_java_string_literal(RAW_UNICODE_ESCAPE), ESCAPED_UNICODE_ESCAPE);
assert_eq!(escape_java_string_literal(r"\u{1F600}"), r"\\u{1F600}");
assert_eq!(escape_java_string_literal(r"\uZZZZ"), r"\\uZZZZ");
}
#[test]
fn escapes_line_terminators_with_named_escapes_never_unicode_escapes() {
assert_eq!(escape_java_string_literal("new\nline"), r"new\nline");
assert_eq!(escape_java_string_literal("carriage\rreturn"), r"carriage\rreturn");
assert_eq!(escape_java_string_literal("tab\there"), r"tab\there");
assert_eq!(escape_java_string_literal("back\u{8}space"), r"back\bspace");
assert_eq!(escape_java_string_literal("form\u{c}feed"), r"form\ffeed");
}
#[test]
fn escapes_remaining_control_characters_as_three_digit_octal() {
assert_eq!(escape_java_string_literal("nul\0byte"), r"nul\000byte");
assert_eq!(escape_java_string_literal("bell\u{7}here"), r"bell\007here");
assert_eq!(escape_java_string_literal("escape\u{1b}[0m"), r"escape\033[0m");
assert_eq!(escape_java_string_literal("delete\u{7f}here"), r"delete\177here");
assert_eq!(escape_java_string_literal("c1\u{9f}here"), r"c1\237here");
}
#[test]
fn octal_escapes_do_not_swallow_a_following_digit() {
assert_eq!(escape_java_string_literal("\u{7}7"), r"\0077");
}
#[test]
fn escapes_non_ascii_as_utf16_unicode_escapes() {
assert_eq!(escape_java_string_literal("caf\u{e9}"), concat!("caf", "\\", "u00E9"));
assert_eq!(
escape_java_string_literal("line\u{2028}separator"),
concat!("line", "\\", "u2028", "separator")
);
}
#[test]
fn escapes_astral_code_points_as_a_surrogate_pair() {
assert_eq!(
escape_java_string_literal("emoji\u{1F600}name"),
concat!("emoji", "\\", "uD83D", "\\", "uDE00", "name")
);
}
#[test]
fn leaves_ordinary_wire_names_untouched() {
for name in ["max_tokens", "top_p", "type", "SCREAMING_NAME", "a-b.c:d"] {
assert_eq!(escape_java_string_literal(name), name, "{name} must pass through");
}
}
#[test]
fn escapes_an_annotation_breakout_payload() {
assert_eq!(
escape_java_string_literal(r#"breakout") String x; //"#),
r#"breakout\") String x; //"#
);
}
#[test]
fn escaped_output_is_always_a_well_formed_java_literal_body() {
for name in HOSTILE_NAMES {
let escaped = escape_java_string_literal(name);
assert!(escaped.is_ascii(), "{name:?} escaped to non-ASCII: {escaped:?}");
assert!(
!escaped.chars().any(char::is_control),
"{name:?} escaped to a raw control character: {escaped:?}"
);
assert_eq!(
unescaped_quote_count(&escaped),
0,
"{name:?} escaped to an unescaped quote: {escaped:?}"
);
assert!(
unicode_escapes_are_well_formed(&escaped),
"{name:?} escaped to a malformed unicode escape: {escaped:?}"
);
}
}
#[test]
fn comment_escaping_cannot_close_the_comment_or_inject_markup() {
for name in HOSTILE_NAMES {
let escaped = escape_java_comment_text(name);
assert!(!escaped.contains("*/"), "{name:?} can close a comment: {escaped:?}");
assert!(
!escaped.contains('<') && !escaped.contains('>'),
"{name:?} leaks javadoc markup: {escaped:?}"
);
assert!(
!escaped.chars().any(char::is_control),
"{name:?} escapes to a control character inside a comment: {escaped:?}"
);
}
assert_eq!(escape_java_comment_text("comment*/close"), "comment*/close");
assert_eq!(
escape_java_comment_text("angle<bracket>and&"),
"angle<bracket>and&amp"
);
}
fn unescaped_quote_count(escaped: &str) -> usize {
let bytes = escaped.as_bytes();
let mut count = 0;
for (index, byte) in bytes.iter().enumerate() {
if *byte != b'"' {
continue;
}
let preceding_backslashes = bytes[..index].iter().rev().take_while(|b| **b == b'\\').count();
if preceding_backslashes.is_multiple_of(2) {
count += 1;
}
}
count
}
fn unicode_escapes_are_well_formed(escaped: &str) -> bool {
let bytes = escaped.as_bytes();
for (index, byte) in bytes.iter().enumerate() {
if *byte != b'u' || index == 0 {
continue;
}
let preceding_backslashes = bytes[..index].iter().rev().take_while(|b| **b == b'\\').count();
if preceding_backslashes.is_multiple_of(2) {
continue;
}
let digits = &bytes[index + 1..];
if digits.len() < 4 || !digits[..4].iter().all(u8::is_ascii_hexdigit) {
return false;
}
}
true
}