|
132 | 132 | r")(?=$|[/?#>])" |
133 | 133 | ) |
134 | 134 | _HTTP_URL = re.compile(r"https?://[^\s<>)\]]+", re.IGNORECASE) |
| 135 | +_URLISH_TOKEN = re.compile(r"https?:[^\s<>\"']+", re.IGNORECASE) |
135 | 136 | _RECOVERY_TAGS = {"0.1.0a1": "v0.1.0-alpha.1+recovery.1"} |
136 | 137 | _FULL_COMMIT = re.compile(r"(?<![0-9a-f])[0-9a-f]{40}(?![0-9a-f])", re.IGNORECASE) |
137 | 138 | _ACTIONS_PREFIX = f"{CANONICAL_REPOSITORY}/actions/runs/" |
|
140 | 141 | re.IGNORECASE, |
141 | 142 | ) |
142 | 143 | _CANONICAL_ACTIONS_URL = re.compile( |
143 | | - rf"(?<![^\s<(]){re.escape(_ACTIONS_PREFIX)}(?P<run>[1-9]\d*)" |
| 144 | + rf"(?<![^\s<(\"']){re.escape(_ACTIONS_PREFIX)}(?P<run>[1-9]\d*)" |
144 | 145 | r"(?:/attempts/(?P<attempt>[1-9]\d*))?" |
145 | 146 | r"(?=$|[\s<>\"')\]}]|[.,;:!?](?=$|\s))" |
146 | 147 | ) |
| 148 | +_RAW_HTML_URL_ATTRIBUTE = re.compile( |
| 149 | + r"(?is)\b(?:href|src)\s*=\s*(?P<quote>['\"])(?P<url>.*?)(?P=quote)" |
| 150 | +) |
| 151 | + |
| 152 | + |
| 153 | +def _actions_path_has_canonical_url( |
| 154 | + text: str, |
| 155 | + path: re.Match[str], |
| 156 | + canonical_matches: list[re.Match[str]], |
| 157 | +) -> bool: |
| 158 | + canonical = next( |
| 159 | + ( |
| 160 | + match |
| 161 | + for match in canonical_matches |
| 162 | + if match.start() <= path.start() and path.end() <= match.end() |
| 163 | + ), |
| 164 | + None, |
| 165 | + ) |
| 166 | + if canonical is None: |
| 167 | + return False |
| 168 | + |
| 169 | + for attribute in _RAW_HTML_URL_ATTRIBUTE.finditer(text): |
| 170 | + if attribute.start("url") <= path.start() and path.end() <= attribute.end("url"): |
| 171 | + return _CANONICAL_ACTIONS_URL.fullmatch(attribute.group("url")) is not None |
| 172 | + |
| 173 | + containing_tokens = [ |
| 174 | + match |
| 175 | + for match in _URLISH_TOKEN.finditer(text) |
| 176 | + if match.start() <= path.start() and path.end() <= match.end() |
| 177 | + ] |
| 178 | + if containing_tokens and min(match.start() for match in containing_tokens) < canonical.start(): |
| 179 | + return False |
| 180 | + |
| 181 | + if canonical.start() == 0: |
| 182 | + return True |
| 183 | + boundary = text[canonical.start() - 1] |
| 184 | + if boundary.isspace(): |
| 185 | + return True |
| 186 | + if boundary == "<": |
| 187 | + return canonical.start() >= 2 and text[canonical.start() - 2].isspace() |
| 188 | + if boundary == "(": |
| 189 | + before_boundary = canonical.start() - 2 |
| 190 | + return ( |
| 191 | + before_boundary < 0 or text[before_boundary].isspace() or text[before_boundary] == "]" |
| 192 | + ) |
| 193 | + return False |
| 194 | + |
| 195 | + |
147 | 196 | _WHEEL_DIGEST = re.compile( |
148 | 197 | r"\bwheel\s+sha256\b[^0-9a-f]{0,96}(?P<digest>[0-9a-f]{64})(?![0-9a-f])", |
149 | 198 | re.IGNORECASE | re.DOTALL, |
@@ -551,7 +600,7 @@ def _identity_violations( |
551 | 600 | ), |
552 | 601 | "exact release commit": re.compile(re.escape(identity.commit), re.IGNORECASE), |
553 | 602 | "exact release workflow URL": re.compile( |
554 | | - rf"(?<![^\s<(]){re.escape(_ACTIONS_PREFIX)}{identity.workflow_run}" |
| 603 | + rf"(?<![^\s<(\"']){re.escape(_ACTIONS_PREFIX)}{identity.workflow_run}" |
555 | 604 | r"(?:/attempts/[1-9]\d*)?" |
556 | 605 | r"(?=$|[\s<>\"')\]}]|[.,;:!?](?=$|\s))" |
557 | 606 | ), |
@@ -701,12 +750,9 @@ def _canonical_actions_run_violations( |
701 | 750 | path_matches = list(_ACTIONS_PATH.finditer(direct)) |
702 | 751 | canonical_matches = list(_CANONICAL_ACTIONS_URL.finditer(direct)) |
703 | 752 | malformed = any( |
704 | | - not any( |
705 | | - url.start() <= path.start() and path.end() <= url.end() for url in canonical_matches |
706 | | - ) |
| 753 | + not _actions_path_has_canonical_url(direct, path, canonical_matches) |
707 | 754 | for path in path_matches |
708 | 755 | ) |
709 | | - |
710 | 756 | direct_paths = Counter((match.group("run"), match.group("attempt")) for match in path_matches) |
711 | 757 | variant_paths = [ |
712 | 758 | Counter( |
|
0 commit comments