forked from lidge-jun/opencodex
-
Notifications
You must be signed in to change notification settings - Fork 0
fix(ci): scan markdown fences in linear time #462
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
luvs01
wants to merge
12
commits into
dev
Choose a base branch
from
codex/propose-fix-for-regex-denial-of-service-ou1mku
base: dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+220
−4
Open
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
06ec553
Merge pull request #3678 from lidge-jun/codex/promote-main-243-01a07240
lidge-jun 116c2ac
Merge commit '44ea9576e27c6be8be7f13a86e32bb349368c54d' into codex/re…
invalid-email-address 07b48da
Merge pull request #3785 from lidge-jun/codex/release-244-main-07c0
lidge-jun bcdf559
chore(release): promote validated 2.45.0 to main [skip ci]
invalid-email-address b0900e5
chore(release): promote 2.45.0 to main (#3813)
lidge-jun 3970601
chore(release): prepare 2.46.0 stable promotion
invalid-email-address bba6322
Merge pull request #3851 from lidge-jun/codex/release-246-main
lidge-jun d32b8d6
fix(ci): scan markdown fences in linear time
luvs01 b40398d
fix(ci): handle CRLF line endings when matching closing markdown fences
luvs01 5e0d098
Merge branch 'dev' into codex/propose-fix-for-regex-denial-of-service…
luvs01 eba3476
fix(ci): pair fence openers by index so an unmatched opener cannot hi…
devin-ai-integration[bot] 0c0bccf
fix(ci): let a shorter closing fence match the way the backreference did
devin-ai-integration[bot] File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -54,8 +54,121 @@ function carryWindow(text, from) { | |
|
|
||
| const TRAILER_RE = /^[ \t]*co-authored-by:[ \t]*(.+)$/gim; | ||
|
|
||
| const FENCED_CODE_RE = /^[ \t]*(\u0060{3,}|~{3,})[\s\S]*?^[ \t]*\1[ \t]*$/gm; | ||
| const INLINE_CODE_RE = /\u0060[^\u0060\n]*\u0060/g; | ||
|
|
||
| /** | ||
| * Remove fenced blocks in linear time. | ||
| * | ||
| * A regex that searches lazily for a closing fence has to retry from every | ||
| * opening-looking line when no close exists. Pull request and commit text is | ||
| * untrusted workflow input, so that quadratic failure mode is significant | ||
| * here. Index the pure fence lines -- the only lines that can close a block -- | ||
| * then walk the lines once: an opener pairs with the longest closing run no | ||
| * longer than its own, and an opener with no such line remains ordinary text, | ||
| * so a later opener can still pair with its own close. | ||
| * | ||
| * Line boundaries follow the same rule the regex's ^ and $ did: CR, LF, and | ||
| * the Unicode separators all end a line, with CRLF as one terminator. The | ||
| * closing-length rule is what the backreference produced by backtracking: it | ||
| * captured the full greedy run first and shortened it one delimiter at a | ||
| * time, so the longest close length still ahead wins and the earliest line | ||
| * carrying it ends the block. | ||
| */ | ||
| function stripFencedCode(text) { | ||
| const lineStarts = [0]; | ||
| const lineEnds = [text.length]; | ||
| for (let i = 0; i < text.length; i++) { | ||
| const c = text[i]; | ||
| if (c === "\n" || c === "\r" || c === "\u2028" || c === "\u2029") { | ||
| lineEnds[lineStarts.length - 1] = i; | ||
| if (c === "\r" && text[i + 1] === "\n") i++; | ||
| lineStarts.push(i + 1); | ||
| lineEnds.push(text.length); | ||
| } | ||
| } | ||
|
|
||
| const openRun = new Array(lineStarts.length).fill(null); | ||
| const closeLists = { "`": new Map(), "~": new Map() }; | ||
| for (let i = 0; i < lineStarts.length; i++) { | ||
| const line = text.slice(lineStarts[i], lineEnds[i]); | ||
| const opening = /^[ \t]*(\u0060{3,}|~{3,})/.exec(line); | ||
| if (opening) openRun[i] = { fence: opening[1][0], len: opening[1].length }; | ||
| const closing = /^[ \t]*(\u0060{3,}|~{3,})[ \t]*$/.exec(line); | ||
| if (closing) { | ||
| const lists = closeLists[closing[1][0]]; | ||
| const len = closing[1].length; | ||
| const list = lists.get(len); | ||
| if (list) list.push(i); | ||
| else lists.set(len, [i]); | ||
| } | ||
| } | ||
|
|
||
| // For each delimiter, the distinct close lengths and a disjoint set that | ||
| // permanently skips a length once every line carrying it is behind the | ||
| // scan. Scanning only moves forward, so each removal is final and total | ||
| // work stays near-linear. | ||
| const fenceIndex = {}; | ||
| for (const fence of ["\u0060", "~"]) { | ||
| const lengths = [...closeLists[fence].keys()].sort((a, b) => a - b); | ||
| fenceIndex[fence] = { | ||
| lengths, | ||
| lists: lengths.map((len) => closeLists[fence].get(len)), | ||
| cursors: new Array(lengths.length).fill(0), | ||
| parent: lengths.map((_, index) => index), | ||
| }; | ||
| } | ||
|
|
||
| // Largest member of index's set still reachable -- the disjoint-set | ||
| // "previous element" trick; a linked root below index is the answer. | ||
| function aliveAt(scan, index) { | ||
| let root = index; | ||
| while (root >= 0 && scan.parent[root] !== root) root = scan.parent[root]; | ||
| while (index >= 0 && scan.parent[index] !== index) { | ||
| const next = scan.parent[index]; | ||
| scan.parent[index] = root; | ||
| index = next; | ||
| } | ||
| return root; | ||
| } | ||
|
|
||
| // First pure-fence line after `after` carrying the longest close length | ||
| // that is at most openerLen; -1 when no close length qualifies. | ||
| function closeFor(fence, openerLen, after) { | ||
| const scan = fenceIndex[fence]; | ||
| let lo = 0; | ||
| let hi = scan.lengths.length; | ||
| while (lo < hi) { | ||
| const mid = (lo + hi) >> 1; | ||
| if (scan.lengths[mid] <= openerLen) lo = mid + 1; | ||
| else hi = mid; | ||
| } | ||
| const pos = lo - 1; | ||
| while (pos >= 0) { | ||
| const index = aliveAt(scan, pos); | ||
| if (index < 0) return -1; | ||
| const list = scan.lists[index]; | ||
| let cursor = scan.cursors[index]; | ||
| while (cursor < list.length && list[cursor] <= after) cursor++; | ||
| scan.cursors[index] = cursor; | ||
| if (cursor < list.length) return list[cursor]; | ||
| scan.parent[index] = aliveAt(scan, index - 1); | ||
| } | ||
| return -1; | ||
|
Comment on lines
+136
to
+156
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| } | ||
|
|
||
| let output = ""; | ||
| let copiedThrough = 0; | ||
| for (let i = 0; i < lineStarts.length; i++) { | ||
| const run = openRun[i]; | ||
| if (run === null) continue; | ||
| const close = closeFor(run.fence, run.len, i); | ||
| if (close === -1) continue; | ||
| output += text.slice(copiedThrough, lineStarts[i]); | ||
| copiedThrough = lineEnds[close]; | ||
| i = close; | ||
| } | ||
| return output + text.slice(copiedThrough); | ||
| } | ||
| /** | ||
| * HTML comments, which GitHub never renders. | ||
| * | ||
|
|
@@ -81,8 +194,7 @@ const HTML_COMMENT_RE = /<!--[\s\S]*?(?:-->|$)/g; | |
| */ | ||
| function strippedText(text) { | ||
| if (typeof text !== "string") return ""; | ||
| return text | ||
| .replace(FENCED_CODE_RE, "") | ||
| return stripFencedCode(text) | ||
| .replace(HTML_COMMENT_RE, "") | ||
| .replace(INLINE_CODE_RE, ""); | ||
| } | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔍 Security review is required before merge
This changes repository automation under
.github/. Repository policy requires explicit security review before merge.Was this helpful? React with 👍 or 👎 to provide feedback.