From 04017581fb1e2383169eab23e08facebafb9c748 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 9 Aug 2024 16:15:44 +0200 Subject: [PATCH] Add all tests imaginable for branch name sanitization --- gix-validate/src/reference.rs | 67 +++++++++-- gix-validate/src/tag.rs | 103 ++++++++++++++--- gix-validate/tests/reference/mod.rs | 169 ++++++++++++++++++++++++++++ gix-validate/tests/tag/mod.rs | 64 ++++++++++- 4 files changed, 378 insertions(+), 25 deletions(-) diff --git a/gix-validate/src/reference.rs b/gix-validate/src/reference.rs index 15a596a8346..35c002d3314 100644 --- a/gix-validate/src/reference.rs +++ b/gix-validate/src/reference.rs @@ -27,35 +27,82 @@ pub mod name { } use bstr::BStr; +use std::borrow::Cow; /// Validate a reference name running all the tests in the book. This disallows lower-case references like `lower`, but also allows /// ones like `HEAD`, and `refs/lower`. pub fn name(path: &BStr) -> Result<&BStr, name::Error> { - validate(path, Mode::Complete) + match validate(path, Mode::Complete)? { + Cow::Borrowed(inner) => Ok(inner), + Cow::Owned(_) => { + unreachable!("Without sanitization, there is no chance a sanitized version is returned.") + } + } } /// Validate a partial reference name. As it is assumed to be partial, names like `some-name` is allowed /// even though these would be disallowed with when using [`name()`]. pub fn name_partial(path: &BStr) -> Result<&BStr, name::Error> { - validate(path, Mode::Partial) + match validate(path, Mode::Partial)? { + Cow::Borrowed(inner) => Ok(inner), + Cow::Owned(_) => { + unreachable!("Without sanitization, there is no chance a sanitized version is returned.") + } + } +} + +/// The infallible version of [`name_partial()`] which instead of failing, alters `path` and returns it to be a valid +/// partial name, which would also pass [`name_partial()`]. +/// +/// Note that an empty `path` is replaced with a `-` in order to be valid. +pub fn name_partial_or_sanitize(path: &BStr) -> Cow<'_, BStr> { + validate(path, Mode::PartialSanitize).expect("BUG: errors cannot happen as any issue is fixed instantly") } enum Mode { Complete, Partial, + /// like Partial, but instead of failing, a sanitized version is returned. + PartialSanitize, } -fn validate(path: &BStr, mode: Mode) -> Result<&BStr, name::Error> { - crate::tag::name(path)?; - if path[0] == b'/' { - return Err(name::Error::StartsWithSlash); +fn validate(path: &BStr, mode: Mode) -> Result, name::Error> { + let mut out = crate::tag::name_inner( + path, + match mode { + Mode::Complete | Mode::Partial => crate::tag::Mode::Validate, + Mode::PartialSanitize => crate::tag::Mode::Sanitize, + }, + )?; + let sanitize = matches!(mode, Mode::PartialSanitize); + if path.get(0) == Some(&b'/') { + if sanitize { + out.to_mut()[0] = b'-'; + } else { + return Err(name::Error::StartsWithSlash); + } } let mut previous = 0; let mut saw_slash = false; - for byte in path.iter() { + let mut out_ofs = 0; + for (mut byte_pos, byte) in path.iter().enumerate() { + byte_pos -= out_ofs; match *byte { - b'/' if previous == b'/' => return Err(name::Error::RepeatedSlash), - b'.' if previous == b'/' => return Err(name::Error::StartsWithDot), + b'/' if previous == b'/' => { + if sanitize { + out.to_mut().remove(byte_pos); + out_ofs += 1; + } else { + return Err(name::Error::RepeatedSlash); + } + } + b'.' if previous == b'/' => { + if sanitize { + out.to_mut()[byte_pos] = b'-'; + } else { + return Err(name::Error::StartsWithDot); + } + } _ => {} } @@ -70,5 +117,5 @@ fn validate(path: &BStr, mode: Mode) -> Result<&BStr, name::Error> { return Err(name::Error::SomeLowercase); } } - Ok(path) + Ok(out) } diff --git a/gix-validate/src/tag.rs b/gix-validate/src/tag.rs index 78202e39a5f..2e71be90b10 100644 --- a/gix-validate/src/tag.rs +++ b/gix-validate/src/tag.rs @@ -1,4 +1,5 @@ -use bstr::BStr; +use bstr::{BStr, ByteSlice}; +use std::borrow::Cow; /// #[allow(clippy::empty_docs)] @@ -33,36 +34,110 @@ pub mod name { /// Assure the given `input` resemble a valid git tag name, which is returned unchanged on success. /// Tag names are provided as names, lik` v1.0` or `alpha-1`, without paths. pub fn name(input: &BStr) -> Result<&BStr, name::Error> { + match name_inner(input, Mode::Validate)? { + Cow::Borrowed(inner) => Ok(inner), + Cow::Owned(_) => { + unreachable!("When validating, the input isn't changed") + } + } +} + +#[derive(Eq, PartialEq)] +pub(crate) enum Mode { + Sanitize, + Validate, +} + +pub(crate) fn name_inner(input: &BStr, mode: Mode) -> Result, name::Error> { + let mut out = Cow::Borrowed(input); + let sanitize = matches!(mode, Mode::Sanitize); if input.is_empty() { - return Err(name::Error::Empty); + return if sanitize { + out.to_mut().push(b'-'); + Ok(out) + } else { + Err(name::Error::Empty) + }; } if *input.last().expect("non-empty") == b'/' { - return Err(name::Error::EndsWithSlash); + if sanitize { + while out.last() == Some(&b'/') { + out.to_mut().pop(); + } + let bytes_from_end = out.to_mut().as_bytes_mut().iter_mut().rev(); + for b in bytes_from_end.take_while(|b| **b == b'/') { + *b = b'-'; + } + } else { + return Err(name::Error::EndsWithSlash); + } } let mut previous = 0; - for byte in input.iter() { + let mut out_ofs = 0; + for (mut byte_pos, byte) in input.iter().enumerate() { + byte_pos -= out_ofs; match byte { b'\\' | b'^' | b':' | b'[' | b'?' | b' ' | b'~' | b'\0'..=b'\x1F' | b'\x7F' => { - return Err(name::Error::InvalidByte { - byte: (&[*byte][..]).into(), - }) + if sanitize { + out.to_mut()[byte_pos] = b'-'; + } else { + return Err(name::Error::InvalidByte { + byte: (&[*byte][..]).into(), + }); + } + } + b'*' => { + if sanitize { + out.to_mut()[byte_pos] = b'-'; + } else { + return Err(name::Error::Asterisk); + } + } + + b'.' if previous == b'.' => { + if sanitize { + out.to_mut().remove(byte_pos); + out_ofs += 1; + } else { + return Err(name::Error::DoubleDot); + } + } + b'{' if previous == b'@' => { + if sanitize { + out.to_mut()[byte_pos] = b'-'; + } else { + return Err(name::Error::ReflogPortion); + } } - b'*' => return Err(name::Error::Asterisk), - b'.' if previous == b'.' => return Err(name::Error::DoubleDot), - b'{' if previous == b'@' => return Err(name::Error::ReflogPortion), _ => {} } previous = *byte; } if input[0] == b'.' { - return Err(name::Error::StartsWithDot); + if sanitize { + out.to_mut()[0] = b'-'; + } else { + return Err(name::Error::StartsWithDot); + } } if input[input.len() - 1] == b'.' { - return Err(name::Error::EndsWithDot); + if sanitize { + let last = out.len() - 1; + out.to_mut()[last] = b'-'; + } else { + return Err(name::Error::EndsWithDot); + } } if input.ends_with(b".lock") { - return Err(name::Error::LockFileSuffix); + if sanitize { + while out.ends_with(b".lock") { + let len_without_suffix = out.len() - b".lock".len(); + out.to_mut().truncate(len_without_suffix); + } + } else { + return Err(name::Error::LockFileSuffix); + } } - Ok(input) + Ok(out) } diff --git a/gix-validate/tests/reference/mod.rs b/gix-validate/tests/reference/mod.rs index e6923617219..f340d6bfff7 100644 --- a/gix-validate/tests/reference/mod.rs +++ b/gix-validate/tests/reference/mod.rs @@ -1,3 +1,14 @@ +macro_rules! mktests { + ($name:ident, $input:expr, $expected:expr) => { + #[test] + fn $name() { + let actual = gix_validate::reference::name_partial_or_sanitize($input.as_bstr()); + assert_eq!(actual.as_ref(), $expected); + assert!(gix_validate::reference::name_partial(actual.as_ref()).is_ok()); + } + }; +} + mod name_partial { mod valid { use bstr::ByteSlice; @@ -11,19 +22,57 @@ mod name_partial { } mktest!(refs_path, b"refs/heads/main"); + mktests!(refs_path_san, b"refs/heads/main", "refs/heads/main"); mktest!(main_worktree_pseudo_ref, b"main-worktree/HEAD"); + mktests!( + main_worktree_pseudo_ref_san, + b"main-worktree/HEAD", + "main-worktree/HEAD" + ); mktest!(main_worktree_ref, b"main-worktree/refs/bisect/good"); + mktests!( + main_worktree_ref_san, + b"main-worktree/refs/bisect/good", + "main-worktree/refs/bisect/good" + ); mktest!(other_worktree_pseudo_ref, b"worktrees/id/HEAD"); + mktests!(other_worktree_pseudo_ref_san, b"worktrees/id/HEAD", "worktrees/id/HEAD"); mktest!(other_worktree_ref, b"worktrees/id/refs/bisect/good"); + mktests!( + other_worktree_ref_san, + b"worktrees/id/refs/bisect/good", + "worktrees/id/refs/bisect/good" + ); mktest!(worktree_private_ref, b"refs/worktree/private"); + mktests!( + worktree_private_ref_san, + b"refs/worktree/private", + "refs/worktree/private" + ); mktest!(refs_path_with_file_extension, b"refs/heads/main.ext"); + mktests!( + refs_path_with_file_extension_san, + b"refs/heads/main.ext", + "refs/heads/main.ext" + ); mktest!(refs_path_underscores_and_dashes, b"refs/heads/main-2nd_ext"); + mktests!( + refs_path_underscores_and_dashes_san, + b"refs/heads/main-2nd_ext", + "refs/heads/main-2nd_ext" + ); mktest!(relative_path, b"etc/foo"); + mktests!(relative_path_san, b"etc/foo", "etc/foo"); mktest!(all_uppercase, b"MAIN"); + mktests!(all_uppercase_san, b"MAIN", "MAIN"); mktest!(all_uppercase_with_underscore, b"NEW_HEAD"); + mktests!(all_uppercase_with_underscore_san, b"NEW_HEAD", "NEW_HEAD"); mktest!(partial_name_lowercase, b"main"); + mktests!(partial_name_lowercase_san, b"main", "main"); mktest!(chinese_utf8, "heads/你好吗".as_bytes()); + mktests!(chinese_utf8_san, "heads/你好吗".as_bytes(), "heads/你好吗"); mktest!(parentheses_special_case_upload_pack, b"(null)"); + mktests!(parentheses_special_case_upload_pack_san, b"(null)", "(null)"); } mod invalid { @@ -47,39 +96,75 @@ mod name_partial { b"refs/../somewhere", RefError::Tag(TagError::DoubleDot) ); + mktests!(refs_path_double_dot_san, b"refs/../somewhere", "refs/-/somewhere"); mktest!( refs_path_name_starts_with_dot, b".refs/somewhere", RefError::Tag(TagError::StartsWithDot) ); + mktest!( + refs_path_name_starts_with_multi_dot, + b"..refs/somewhere", + RefError::Tag(TagError::DoubleDot) + ); + mktests!( + refs_path_name_starts_with_multi_dot_san, + b"..refs/somewhere", + "-refs/somewhere" + ); + mktests!( + refs_path_name_starts_with_dot_san, + b".refs/somewhere", + "-refs/somewhere" + ); mktest!( refs_path_component_is_singular_dot, b"refs/./still-inside-but-not-cool", RefError::StartsWithDot ); + mktests!( + refs_path_component_is_singular_dot_san, + b"refs/./still-inside-but-not-cool", + "refs/-/still-inside-but-not-cool" + ); mktest!(any_path_starts_with_slash, b"/etc/foo", RefError::StartsWithSlash); + mktests!(any_path_starts_with_slash_san, b"/etc/foo", "-etc/foo"); mktest!(empty_path, b"", RefError::Tag(TagError::Empty)); + mktests!(empty_path_san, b"", "-"); mktest!(refs_starts_with_slash, b"/refs/heads/main", RefError::StartsWithSlash); + mktests!(refs_starts_with_slash_san, b"/refs/heads/main", "-refs/heads/main"); mktest!( ends_with_slash, b"refs/heads/main/", RefError::Tag(TagError::EndsWithSlash) ); + mktests!(ends_with_slash_san, b"refs/heads/main/", "refs/heads/main"); mktest!( path_with_duplicate_slashes, b"refs//heads/main", RefError::RepeatedSlash ); + mktests!(path_with_duplicate_slashes_san, b"refs//heads/main", "refs/heads/main"); mktest!( path_with_spaces, b"refs//heads/name with spaces", RefError::Tag(TagError::InvalidByte { .. }) ); + mktests!( + path_with_spaces_san, + b"refs//heads////name with spaces", + "refs/heads/name-with-spaces" + ); mktest!( path_with_backslashes, b"refs\\heads/name with spaces", RefError::Tag(TagError::InvalidByte { .. }) ); + mktests!( + path_with_backslashes_san, + b"refs\\heads/name with spaces", + "refs-heads/name-with-spaces" + ); } } @@ -96,18 +181,59 @@ mod name { } mktest!(main_worktree_pseudo_ref, b"main-worktree/HEAD"); + mktests!( + main_worktree_pseudo_ref_san, + b"main-worktree/HEAD", + "main-worktree/HEAD" + ); mktest!(main_worktree_ref, b"main-worktree/refs/bisect/good"); + mktests!( + main_worktree_ref_san, + b"main-worktree/refs/bisect/good", + "main-worktree/refs/bisect/good" + ); mktest!(other_worktree_pseudo_ref, b"worktrees/id/HEAD"); + mktests!(other_worktree_pseudo_ref_san, b"worktrees/id/HEAD", "worktrees/id/HEAD"); mktest!(other_worktree_ref, b"worktrees/id/refs/bisect/good"); + mktests!( + other_worktree_ref_san, + b"worktrees/id/refs/bisect/good", + "worktrees/id/refs/bisect/good" + ); mktest!(worktree_private_ref, b"refs/worktree/private"); + mktests!( + worktree_private_ref_san, + b"refs/worktree/private", + "refs/worktree/private" + ); mktest!(refs_path, b"refs/heads/main"); + mktests!(refs_path_san, b"refs/heads/main", "refs/heads/main"); mktest!(refs_path_with_file_extension, b"refs/heads/main.ext"); + mktests!( + refs_path_with_file_extension_san, + b"refs/heads/main.ext", + "refs/heads/main.ext" + ); mktest!(refs_path_underscores_and_dashes, b"refs/heads/main-2nd_ext"); + mktests!( + refs_path_underscores_and_dashes_san, + b"refs/heads/main-2nd_ext", + "refs/heads/main-2nd_ext" + ); mktest!(relative_path, b"etc/foo"); + mktests!(relative_path_san, b"etc/foo", "etc/foo"); mktest!(all_uppercase, b"MAIN"); + mktests!(all_uppercase_san, b"MAIN", "MAIN"); mktest!(all_uppercase_with_underscore, b"NEW_HEAD"); + mktests!(all_uppercase_with_underscore_san, b"NEW_HEAD", "NEW_HEAD"); mktest!(chinese_utf8, "refs/heads/你好吗".as_bytes()); + mktests!(chinese_utf8_san, "refs/heads/你好吗".as_bytes(), "refs/heads/你好吗"); mktest!(dot_in_directory_component, b"this./totally./works"); + mktests!( + dot_in_directory_component_san, + b"this./totally./works", + "this./totally./works" + ); } mod invalid { @@ -131,42 +257,85 @@ mod name { b"refs/../somewhere", RefError::Tag(TagError::DoubleDot) ); + mktests!(refs_path_double_dot_san, b"refs/../somewhere", "refs/-/somewhere"); mktest!(refs_name_special_case_upload_pack, b"(null)", RefError::SomeLowercase); + mktests!(refs_name_special_case_upload_pack_san, b"(null)", "(null)"); mktest!( refs_path_name_starts_with_dot, b".refs/somewhere", RefError::Tag(TagError::StartsWithDot) ); + mktests!( + refs_path_name_starts_with_dot_san, + b".refs/somewhere", + "-refs/somewhere" + ); mktest!( refs_path_name_starts_with_dot_in_name, b"refs/.somewhere", RefError::StartsWithDot ); + mktests!( + refs_path_name_starts_with_dot_in_name_san, + b"refs/.somewhere", + "refs/-somewhere" + ); mktest!( refs_path_name_ends_with_dot_in_name, b"refs/somewhere.", RefError::Tag(TagError::EndsWithDot) ); + mktests!( + refs_path_name_ends_with_dot_in_name_san, + b"refs/somewhere.", + "refs/somewhere-" + ); mktest!( refs_path_component_is_singular_dot, b"refs/./still-inside-but-not-cool", RefError::StartsWithDot ); + mktests!( + refs_path_component_is_singular_dot_an, + b"refs/./still-inside-but-not-cool", + "refs/-/still-inside-but-not-cool" + ); mktest!(capitalized_name_without_path, b"Main", RefError::SomeLowercase); + mktests!(capitalized_name_without_path_san, b"Main", "Main"); mktest!(lowercase_name_without_path, b"main", RefError::SomeLowercase); + mktests!(lowercase_name_without_path_san, b"main", "main"); mktest!(any_path_starts_with_slash, b"/etc/foo", RefError::StartsWithSlash); + mktests!(any_path_starts_with_slash_san, b"/etc/foo", "-etc/foo"); mktest!(empty_path, b"", RefError::Tag(TagError::Empty)); + mktests!(empty_path_san, b"", "-"); mktest!(refs_starts_with_slash, b"/refs/heads/main", RefError::StartsWithSlash); + mktests!(refs_starts_with_slash_san, b"/refs/heads/main", "-refs/heads/main"); mktest!( ends_with_slash, b"refs/heads/main/", RefError::Tag(TagError::EndsWithSlash) ); + mktests!(ends_with_slash_san, b"refs/heads/main/", "refs/heads/main"); + mktest!( + ends_with_slash_multiple, + b"refs/heads/main///", + RefError::Tag(TagError::EndsWithSlash) + ); + mktests!( + ends_with_slash_multiple_san, + b"refs/heads/main///", + "refs/heads/main---" + ); mktest!( a_path_with_duplicate_slashes, b"refs//heads/main", RefError::RepeatedSlash ); + mktests!( + a_path_with_duplicate_slashes_san, + b"refs//heads/main", + "refs/heads/main" + ); } } diff --git a/gix-validate/tests/tag/mod.rs b/gix-validate/tests/tag/mod.rs index 96838071c3f..86f43bb377d 100644 --- a/gix-validate/tests/tag/mod.rs +++ b/gix-validate/tests/tag/mod.rs @@ -1,4 +1,15 @@ mod name { + macro_rules! mktests { + ($name:ident, $input:expr, $expected:expr) => { + #[test] + fn $name() { + let actual = gix_validate::reference::name_partial_or_sanitize($input.as_bstr()); + assert_eq!(actual.as_ref(), $expected); + assert!(gix_validate::reference::name_partial(actual.as_ref()).is_ok()); + } + }; + } + mod valid { use bstr::ByteSlice; @@ -10,16 +21,28 @@ mod name { } }; } - mktest!(an_at_sign, b"@"); + mktests!(an_at_sign_san, b"@", "@"); mktest!(chinese_utf8, "你好吗".as_bytes()); + mktests!(chinese_utf8_san, "你好吗".as_bytes(), "你好吗"); mktest!(non_text, "😅🙌".as_bytes()); + mktests!(non_text_san, "😅🙌".as_bytes(), "😅🙌"); mktest!(contains_an_at, b"hello@foo"); + mktests!(contains_an_at_san, b"hello@foo", "hello@foo"); mktest!(contains_dot_lock, b"file.lock.ext"); + mktests!(contains_dot_lock_san, b"file.lock.ext", "file.lock.ext"); mktest!(contains_brackets, b"this_{is-fine}_too"); + mktests!(contains_brackets_san, b"this_{is-fine}_too", "this_{is-fine}_too"); mktest!(contains_brackets_and_at, b"this_{@is-fine@}_too"); + mktests!( + contains_brackets_and_at_san, + b"this_{@is-fine@}_too", + "this_{@is-fine@}_too" + ); mktest!(dot_in_the_middle, b"token.other"); + mktests!(dot_in_the_middle_san, b"token.other", "token.other"); mktest!(slash_inbetween, b"hello/world"); + mktests!(slash_inbetween_san, b"hello/world", "hello/world"); } mod invalid { @@ -48,34 +71,73 @@ mod name { }; } mktest!(contains_ref_log_portion, b"this_looks_like_a_@{reflog}", ReflogPortion); + mktests!( + contains_ref_log_portion_san, + b"this_looks_like_a_@{reflog}", + "this_looks_like_a_@-reflog}" + ); mktest!(suffix_is_dot_lock, b"prefix.lock", LockFileSuffix); + mktest!(too_many_dots, b"......", DoubleDot); + mktests!(too_many_dots_san, b"......", "-"); + mktests!(too_many_dots_and_slashes_san, b"//....///....///", "-"); + mktests!(suffix_is_dot_lock_san, b"prefix.lock", "prefix"); + mktest!(suffix_is_dot_lock_multiple, b"prefix.lock.lock", LockFileSuffix); + mktests!(suffix_is_dot_lock_multiple_san, b"prefix.lock.lock", "prefix"); mktest!(ends_with_slash, b"prefix/", EndsWithSlash); + mktests!(ends_with_slash_san, b"prefix/", "prefix"); mktest!(is_dot_lock, b".lock", StartsWithDot); + mktests!(is_dot_lock_san, b".lock", "-lock"); mktest!(contains_double_dot, b"with..double-dot", DoubleDot); + mktests!(contains_double_dot_san, b"with..double-dot", "with.double-dot"); mktest!(starts_with_double_dot, b"..with-double-dot", DoubleDot); + mktests!(starts_with_double_dot_san, b"..with-double-dot", "-with-double-dot"); mktest!(ends_with_double_dot, b"with-double-dot..", DoubleDot); + mktests!(ends_with_double_dot_san, b"with-double-dot..", "with-double-dot-"); mktest!(starts_with_asterisk, b"*suffix", Asterisk); + mktests!(starts_with_asterisk_san, b"*suffix", "-suffix"); mktest!(ends_with_asterisk, b"prefix*", Asterisk); + mktests!(ends_with_asterisk_san, b"prefix*", "prefix-"); mktest!(contains_asterisk, b"prefix*suffix", Asterisk); + mktests!(contains_asterisk_san, b"prefix*suffix", "prefix-suffix"); mktestb!(contains_null, b"prefix\0suffix"); + mktests!(contains_null_san, b"prefix\0suffix", "prefix-suffix"); mktestb!(contains_bell, b"prefix\x07suffix"); + mktests!(contains_bell_san, b"prefix\x07suffix", "prefix-suffix"); mktestb!(contains_backspace, b"prefix\x08suffix"); + mktests!(contains_backspace_san, b"prefix\x08suffix", "prefix-suffix"); mktestb!(contains_vertical_tab, b"prefix\x0bsuffix"); + mktests!(contains_vertical_tab_san, b"prefix\x0bsuffix", "prefix-suffix"); mktestb!(contains_form_feed, b"prefix\x0csuffix"); + mktests!(contains_form_feed_san, b"prefix\x0csuffix", "prefix-suffix"); mktestb!(contains_ctrl_z, b"prefix\x1asuffix"); + mktests!(contains_ctrl_z_san, b"prefix\x1asuffix", "prefix-suffix"); mktestb!(contains_esc, b"prefix\x1bsuffix"); + mktests!(contains_esc_san, b"prefix\x1bsuffix", "prefix-suffix"); mktestb!(contains_colon, b"prefix:suffix"); + mktests!(contains_colon_san, b"prefix:suffix", "prefix-suffix"); mktestb!(contains_questionmark, b"prefix?suffix"); + mktests!(contains_questionmark_san, b"prefix?suffix", "prefix-suffix"); mktestb!(contains_open_bracket, b"prefix[suffix"); + mktests!(contains_open_bracket_san, b"prefix[suffix", "prefix-suffix"); mktestb!(contains_backslash, b"prefix\\suffix"); + mktests!(contains_backslash_san, b"prefix\\suffix", "prefix-suffix"); mktestb!(contains_circumflex, b"prefix^suffix"); + mktests!(contains_circumflex_san, b"prefix^suffix", "prefix-suffix"); mktestb!(contains_tilde, b"prefix~suffix"); + mktests!(contains_tilde_san, b"prefix~suffix", "prefix-suffix"); mktestb!(contains_space, b"prefix suffix"); + mktests!(contains_space_san, b"prefix suffix", "prefix-suffix"); mktestb!(contains_tab, b"prefix\tsuffix"); + mktests!(contains_tab_san, b"prefix\tsuffix", "prefix-suffix"); mktestb!(contains_newline, b"prefix\nsuffix"); + mktests!(contains_newline_san, b"prefix\nsuffix", "prefix-suffix"); mktestb!(contains_carriage_return, b"prefix\rsuffix"); + mktests!(contains_carriage_return_san, b"prefix\rsuffix", "prefix-suffix"); mktest!(starts_with_dot, b".with-dot", StartsWithDot); + mktests!(starts_with_dot_san, b".with-dot", "-with-dot"); mktest!(ends_with_dot, b"with-dot.", EndsWithDot); + mktests!(ends_with_dot_san, b"with-dot.", "with-dot-"); mktest!(empty, b"", Empty); + mktests!(empty_san, b"", "-"); } }