Skip to content

Commit 2177408

Browse files
Don't crash during patch creation if there's a massive single hunk (#595)
* Add failing test * Fix crash, add a TODO about similar brokenness in merge.js * Flesh out test * Add release notes * Use bold for key points in release notes again
1 parent b5d7fa4 commit 2177408

File tree

4 files changed

+33
-8
lines changed

4 files changed

+33
-8
lines changed

release-notes.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22

33
## 8.0.0 (pending release)
44

5-
- [#580](https://github.com/kpdecker/jsdiff/pull/580) Multiple tweaks to `diffSentences`:
5+
- [#580](https://github.com/kpdecker/jsdiff/pull/580) **Multiple tweaks to `diffSentences`**:
66
* tokenization no longer takes quadratic time on pathological inputs (reported as a ReDOS vulnerability by Snyk); is now linear instead
77
* the final sentence in the string is now handled the same by the tokenizer regardless of whether it has a trailing punctuation mark or not. (Previously, "foo. bar." tokenized to `["foo.", " ", "bar."]` but "foo. bar" tokenized to `["foo.", " bar"]` - i.e. whether the space between sentences was treated as a separate token depended upon whether the final sentence had trailing punctuation or not. This was arbitrary and surprising; it is no longer the case.)
88
* in a string that starts with a sentence end, like "! hello.", the "!" is now treated as a separate sentence
99
* the README now correctly documents the tokenization behaviour (it was wrong before)
10-
- [#581](https://github.com/kpdecker/jsdiff/pull/581) - fix some regex operations used for tokenization in `diffWords` taking O(n^2) time in pathological cases
10+
- [#581](https://github.com/kpdecker/jsdiff/pull/581) - **fixed some regex operations used for tokenization in `diffWords` taking O(n^2) time** in pathological cases
11+
- [#595](https://github.com/kpdecker/jsdiff/pull/595) - **fixed a crash in patch creation functions when handling a single hunk consisting of a very large number (e.g. >130k) of lines**. (This was caused by spreading indefinitely-large arrays to `.push()` using `.apply` or the spread operator and hitting the JS-implementation-specific limit on the maximum number of arguments to a function, as shown at https://stackoverflow.com/a/56809779/1709587; thus the exact threshold to hit the error will depend on the environment in which you were running JsDiff.)
1112

1213
## 7.0.0
1314

src/patch/create.js

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,9 @@ export function structuredPatch(oldFileName, newFileName, oldStr, newStr, oldHea
6868
}
6969

7070
// Output our changes
71-
curRange.push(... lines.map(function(entry) {
72-
return (current.added ? '+' : '-') + entry;
73-
}));
71+
for (const line of lines) {
72+
curRange.push((current.added ? '+' : '-') + line);
73+
}
7474

7575
// Track the updated file position
7676
if (current.added) {
@@ -84,11 +84,15 @@ export function structuredPatch(oldFileName, newFileName, oldStr, newStr, oldHea
8484
// Close out any changes that have been output (or join overlapping)
8585
if (lines.length <= options.context * 2 && i < diff.length - 2) {
8686
// Overlapping
87-
curRange.push(... contextLines(lines));
87+
for (const line of contextLines(lines)) {
88+
curRange.push(line);
89+
}
8890
} else {
8991
// end the range and output
9092
let contextSize = Math.min(lines.length, options.context);
91-
curRange.push(... contextLines(lines.slice(0, contextSize)));
93+
for (const line of contextLines(lines.slice(0, contextSize))) {
94+
curRange.push(line);
95+
}
9296

9397
let hunk = {
9498
oldStart: oldRangeStart,
@@ -159,7 +163,9 @@ export function formatPatch(diff) {
159163
+ ' +' + hunk.newStart + ',' + hunk.newLines
160164
+ ' @@'
161165
);
162-
ret.push.apply(ret, hunk.lines);
166+
for (const line of hunk.lines) {
167+
ret.push(line);
168+
}
163169
}
164170

165171
return ret.join('\n') + '\n';

src/patch/merge.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,8 @@ function cloneHunk(hunk, offset) {
136136
};
137137
}
138138

139+
// TODO: Fix use of push(... ) pattern here which probably trigger an error for really big changes
140+
// due to the maximum argument limit
139141
function mergeLines(hunk, mineOffset, mineLines, theirOffset, theirLines) {
140142
// This will generally result in a conflicted hunk, but there are cases where the context
141143
// is the only overlap where we can successfully merge the content here.

test/patch/create.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,22 @@ describe('patch/create', function() {
671671
expect(createPatch('test', 'line1\nline2\nline3\n', 'lineX\nlineY\nlineZ\nline42\n', 'header1', 'header2', {maxEditLength: 1})).to.be.undefined;
672672
});
673673

674+
it('should not crash when encountering an enormous hunk', function() {
675+
// Regression test; we used to crash here due to lines like
676+
// ret.push.apply(ret, hunk.lines);
677+
// and
678+
// curRange.push(... contextLines(lines.slice(0, contextSize)));
679+
// in the patch creation code that fail in practice due to JS implementations having a
680+
// maximum argument count.
681+
expect(createTwoFilesPatch('foo', 'bar', '', 'foo\n'.repeat(1000000))).to.equal(
682+
'===================================================================\n'
683+
+ '--- foo\n'
684+
+ '+++ bar\n'
685+
+ '@@ -0,0 +1,1000000 @@\n'
686+
+ '+foo\n'.repeat(1000000)
687+
);
688+
});
689+
674690
describe('ignoreWhitespace', function() {
675691
it('ignoreWhitespace: false', function() {
676692
const expectedResult =

0 commit comments

Comments
 (0)