@@ -17,6 +17,9 @@ class WP_HTML_Decoder {
1717 * of how it might be encoded in HTML. For instance, `http:` could be represented as `http:`
1818 * or as `http:` or as `http:` or as `http:`, or in many other ways.
1919 *
20+ * This is equivalent to a byte-prefix test against the decoded attribute value, without
21+ * the need to allocate and decode the full string.
22+ *
2023 * Example:
2124 *
2225 * $value = 'http://wordpress.org/';
@@ -53,24 +56,71 @@ public static function attribute_starts_with( $haystack, $search_text, $case_sen
5356 return false ;
5457 }
5558
56- // If there's no character reference but the character do match, then it could still match.
59+ // If there's no character reference but the characters do match, then it could still match.
5760 if ( null === $ next_chunk && $ chars_match ) {
5861 ++$ haystack_at ;
5962 ++$ search_at ;
6063 continue ;
6164 }
6265
63- // If there is a character reference, then the decoded value must exactly match what follows in the search string.
64- if ( 0 !== substr_compare ( $ search_text , $ next_chunk , $ search_at , strlen ( $ next_chunk ), $ loose_case ) ) {
66+ /**
67+ * The decoded character reference in `$next_chunk` must be compared with the
68+ * corresponding `$search_text` bytes checking for matching prefixes. The remaining
69+ * search text may be shorter than the decoded chunk, in which case a partial match
70+ * satisfies the prefix. Otherwise, if the decoded chunk is fully matched, the
71+ * comparison must continue after advancing the appropriate byte lengths: the character
72+ * reference token length in the haystack and the decoded chunk length in the
73+ * search text.
74+ *
75+ * For example, consider searches that have reached the character reference
76+ * `fj` (7 bytes), decoded into the 2-byte chunk `fj`:
77+ *
78+ * $haystack_at
79+ * │
80+ * │ ┌─after matching `fj` continue here
81+ * │ │ (advance by $token_length, 7 bytes)
82+ * ↓ ↓
83+ * Haystack: startfjord
84+ * ╰──┬──╯
85+ * fj - the decoded chunk, tested against the search text.
86+ *
87+ * $search_at
88+ * │
89+ * │ ┌─after matching `fj` continue here
90+ * │ │ (advance by $match_length, 2 bytes)
91+ * ↓ ↓
92+ * Search A: startfjord Compare 2 bytes: `fj` matches,
93+ * continue matching at `o`.
94+ *
95+ * $search_at
96+ * ↓
97+ * Search B: startf Compare 1 byte: `f` matches and the
98+ * search text is exhausted — prefix confirmed.
99+ *
100+ * $search_at
101+ * ↓
102+ * Search C: startfr Compare 2 bytes: `fj` differs
103+ * from `fr`, no match is possible.
104+ *
105+ * The `min()` is required in both directions: Search A fails if the
106+ * comparison length comes from the search text, Search B if it comes
107+ * from the chunk.
108+ *
109+ * After a match each cursor must advance by the appropriate length, the haystack
110+ * cursor by the character reference token length, and the search cursor by the
111+ * matched length.
112+ */
113+ $ match_length = min ( strlen ( $ next_chunk ), $ search_length - $ search_at );
114+ if ( 0 !== substr_compare ( $ search_text , $ next_chunk , $ search_at , $ match_length , $ loose_case ) ) {
65115 return false ;
66116 }
67117
68118 // The character reference matched, so continue checking.
69119 $ haystack_at += $ token_length ;
70- $ search_at += strlen ( $ next_chunk ) ;
120+ $ search_at += $ match_length ;
71121 }
72122
73- return true ;
123+ return $ search_at === $ search_length ;
74124 }
75125
76126 /**
0 commit comments