Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
43.59% covered (danger)
43.59%
85 / 195
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
diff_engine
43.59% covered (danger)
43.59%
85 / 195
0.00% covered (danger)
0.00%
0 / 5
1858.32
0.00% covered (danger)
0.00%
0 / 1
 diff
81.16% covered (warning)
81.16%
56 / 69
0.00% covered (danger)
0.00%
0 / 1
41.73
 _diag
0.00% covered (danger)
0.00%
0 / 49
0.00% covered (danger)
0.00%
0 / 1
420
 _lcs_pos
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
30
 _compareseq
35.00% covered (danger)
35.00%
7 / 20
0.00% covered (danger)
0.00%
0 / 1
59.41
 _shift_boundaries
52.38% covered (warning)
52.38%
22 / 42
0.00% covered (danger)
0.00%
0 / 1
105.72
1<?php
2/**
3 *
4 * This file is part of the phpBB Forum Software package.
5 *
6 * @copyright (c) phpBB Limited <https://www.phpbb.com>
7 * @license GNU General Public License, version 2 (GPL-2.0)
8 *
9 * For full copyright and license information, please see
10 * the docs/CREDITS.txt file.
11 *
12 */
13
14/**
15 * @ignore
16 */
17if (!defined('IN_PHPBB'))
18{
19    exit;
20}
21
22/**
23 * Code from pear.php.net, Text_Diff-1.1.0 package
24 * http://pear.php.net/package/Text_Diff/ (native engine)
25 *
26 * Modified by phpBB Limited to meet our coding standards
27 * and being able to integrate into phpBB
28 *
29 * Class used internally by Text_Diff to actually compute the diffs. This
30 * class is implemented using native PHP code.
31 *
32 * The algorithm used here is mostly lifted from the perl module
33 * Algorithm::Diff (version 1.06) by Ned Konz, which is available at:
34 * http://www.perl.com/CPAN/authors/id/N/NE/NEDKONZ/Algorithm-Diff-1.06.zip
35 *
36 * More ideas are taken from: http://www.ics.uci.edu/~eppstein/161/960229.html
37 *
38 * Some ideas (and a bit of code) are taken from analyze.c, of GNU
39 * diffutils-2.7, which can be found at:
40 * ftp://gnudist.gnu.org/pub/gnu/diffutils/diffutils-2.7.tar.gz
41 *
42 * Some ideas (subdivision by NCHUNKS > 2, and some optimizations) are from
43 * Geoffrey T. Dairiki <dairiki@dairiki.org>. The original PHP version of this
44 * code was written by him, and is used/adapted with his permission.
45 *
46 * Copyright 2004-2008 The Horde Project (http://www.horde.org/)
47 *
48 * @author  Geoffrey T. Dairiki <dairiki@dairiki.org>
49 * @package diff
50 *
51 * @access private
52 */
53class diff_engine
54{
55    /** var array */
56    protected $in_seq;
57
58    /** var int */
59    protected $lcs;
60
61    /** var array */
62    protected $seq;
63
64    /**
65     * bool
66     *
67     * If set to true we trim all lines before we compare them. This ensures that sole space/tab changes do not trigger diffs.
68     */
69    protected $skip_whitespace_changes = true;
70
71    /** var array */
72    protected $xchanged;
73
74    /** var array */
75    protected $xind;
76
77    /** var array */
78    protected $xv;
79
80    /** var array */
81    protected $ychanged;
82
83    /** var array */
84    protected $yind;
85
86    /** var array */
87    protected $yv;
88
89    function diff(&$from_lines, &$to_lines, $preserve_cr = true)
90    {
91        /*
92         * Remove empty lines...
93         * If preserve_cr is true, we basically only change \r\n and bare \r to \n to get the same carriage returns for both files
94         * If it is false, we try to only use \n once per line and ommit all empty lines to be able to get a proper data diff
95         */
96        if (is_array($from_lines))
97        {
98            $from_lines = implode("\n", $from_lines);
99        }
100
101        if (is_array($to_lines))
102        {
103            $to_lines = implode("\n", $to_lines);
104        }
105
106        if ($preserve_cr)
107        {
108            $from_lines = explode("\n", str_replace("\r", "\n", str_replace("\r\n", "\n", $from_lines)));
109            $to_lines = explode("\n", str_replace("\r", "\n", str_replace("\r\n", "\n", $to_lines)));
110        }
111        else
112        {
113            $from_lines = explode("\n", preg_replace('#[\n\r]+#', "\n", $from_lines));
114            $to_lines = explode("\n", preg_replace('#[\n\r]+#', "\n", $to_lines));
115        }
116
117        $n_from = count($from_lines);
118        $n_to = count($to_lines);
119
120        $this->xchanged = $this->ychanged = $this->xv = $this->yv = $this->xind = $this->yind = [];
121        unset($this->seq, $this->in_seq, $this->lcs);
122
123        // Skip leading common lines.
124        for ($skip = 0; $skip < $n_from && $skip < $n_to; $skip++)
125        {
126            if (trim($from_lines[$skip]) !== trim($to_lines[$skip]))
127            {
128                break;
129            }
130            $this->xchanged[$skip] = $this->ychanged[$skip] = false;
131        }
132
133        // Skip trailing common lines.
134        $xi = $n_from;
135        $yi = $n_to;
136
137        for ($endskip = 0; --$xi > $skip && --$yi > $skip; $endskip++)
138        {
139            if (trim($from_lines[$xi]) !== trim($to_lines[$yi]))
140            {
141                break;
142            }
143            $this->xchanged[$xi] = $this->ychanged[$yi] = false;
144        }
145
146        // Ignore lines which do not exist in both files.
147        for ($xi = $skip; $xi < $n_from - $endskip; $xi++)
148        {
149            if ($this->skip_whitespace_changes) $xhash[trim($from_lines[$xi])] = 1; else $xhash[$from_lines[$xi]] = 1;
150        }
151
152        for ($yi = $skip; $yi < $n_to - $endskip; $yi++)
153        {
154            $line = ($this->skip_whitespace_changes) ? trim($to_lines[$yi]) : $to_lines[$yi];
155
156            if (($this->ychanged[$yi] = empty($xhash[$line])))
157            {
158                continue;
159            }
160            $yhash[$line] = 1;
161            $this->yv[] = $line;
162            $this->yind[] = $yi;
163        }
164
165        for ($xi = $skip; $xi < $n_from - $endskip; $xi++)
166        {
167            $line = ($this->skip_whitespace_changes) ? trim($from_lines[$xi]) : $from_lines[$xi];
168
169            if (($this->xchanged[$xi] = empty($yhash[$line])))
170            {
171                continue;
172            }
173            $this->xv[] = $line;
174            $this->xind[] = $xi;
175        }
176
177        // Find the LCS.
178        $this->_compareseq(0, count($this->xv), 0, count($this->yv));
179
180        // Merge edits when possible.
181        if ($this->skip_whitespace_changes)
182        {
183            $from_lines_clean = array_map('trim', $from_lines);
184            $to_lines_clean = array_map('trim', $to_lines);
185
186            $this->_shift_boundaries($from_lines_clean, $this->xchanged, $this->ychanged);
187            $this->_shift_boundaries($to_lines_clean, $this->ychanged, $this->xchanged);
188
189            unset($from_lines_clean, $to_lines_clean);
190        }
191        else
192        {
193            $this->_shift_boundaries($from_lines, $this->xchanged, $this->ychanged);
194            $this->_shift_boundaries($to_lines, $this->ychanged, $this->xchanged);
195        }
196
197        // Compute the edit operations.
198        $edits = array();
199        $xi = $yi = 0;
200
201        while ($xi < $n_from || $yi < $n_to)
202        {
203            // Skip matching "snake".
204            $copy = array();
205
206            while ($xi < $n_from && $yi < $n_to && !$this->xchanged[$xi] && !$this->ychanged[$yi])
207            {
208                $copy[] = $from_lines[$xi++];
209                $yi++;
210            }
211
212            if ($copy)
213            {
214                $edits[] = new diff_op_copy($copy);
215            }
216
217            // Find deletes & adds.
218            $delete = array();
219            while ($xi < $n_from && $this->xchanged[$xi])
220            {
221                $delete[] = $from_lines[$xi++];
222            }
223
224            $add = array();
225            while ($yi < $n_to && $this->ychanged[$yi])
226            {
227                $add[] = $to_lines[$yi++];
228            }
229
230            if ($delete && $add)
231            {
232                $edits[] = new diff_op_change($delete, $add);
233            }
234            else if ($delete)
235            {
236                $edits[] = new diff_op_delete($delete);
237            }
238            else if ($add)
239            {
240                $edits[] = new diff_op_add($add);
241            }
242        }
243
244        return $edits;
245    }
246
247    /**
248    * Divides the Largest Common Subsequence (LCS) of the sequences (XOFF,
249    * XLIM) and (YOFF, YLIM) into NCHUNKS approximately equally sized segments.
250    *
251    * Returns (LCS, PTS).  LCS is the length of the LCS. PTS is an array of
252    * NCHUNKS+1 (X, Y) indexes giving the diving points between sub
253    * sequences.  The first sub-sequence is contained in (X0, X1), (Y0, Y1),
254    * the second in (X1, X2), (Y1, Y2) and so on.  Note that (X0, Y0) ==
255    * (XOFF, YOFF) and (X[NCHUNKS], Y[NCHUNKS]) == (XLIM, YLIM).
256    *
257    * This function assumes that the first lines of the specified portions of
258    * the two files do not match, and likewise that the last lines do not
259    * match.  The caller must trim matching lines from the beginning and end
260    * of the portions it is going to specify.
261    */
262    function _diag($xoff, $xlim, $yoff, $ylim, $nchunks)
263    {
264        $flip = false;
265
266        if ($xlim - $xoff > $ylim - $yoff)
267        {
268            // Things seems faster (I'm not sure I understand why) when the shortest sequence is in X.
269            $flip = true;
270            list($xoff, $xlim, $yoff, $ylim) = array($yoff, $ylim, $xoff, $xlim);
271        }
272
273        if ($flip)
274        {
275            for ($i = $ylim - 1; $i >= $yoff; $i--)
276            {
277                $ymatches[$this->xv[$i]][] = $i;
278            }
279        }
280        else
281        {
282            for ($i = $ylim - 1; $i >= $yoff; $i--)
283            {
284                $ymatches[$this->yv[$i]][] = $i;
285            }
286        }
287
288        $this->lcs = 0;
289        $this->seq[0]= $yoff - 1;
290        $this->in_seq = array();
291        $ymids[0] = array();
292
293        $numer = $xlim - $xoff + $nchunks - 1;
294        $x = $xoff;
295
296        for ($chunk = 0; $chunk < $nchunks; $chunk++)
297        {
298            if ($chunk > 0)
299            {
300                for ($i = 0; $i <= $this->lcs; $i++)
301                {
302                    $ymids[$i][$chunk - 1] = $this->seq[$i];
303                }
304            }
305
306            $x1 = $xoff + (int)(($numer + ($xlim - $xoff) * $chunk) / $nchunks);
307
308            for (; $x < $x1; $x++)
309            {
310                $line = $flip ? $this->yv[$x] : $this->xv[$x];
311                if (empty($ymatches[$line]))
312                {
313                    continue;
314                }
315                $matches = $ymatches[$line];
316
317                reset($matches);
318                while ($y = current($matches))
319                {
320                    next($matches);
321                    if (empty($this->in_seq[$y]))
322                    {
323                        $k = $this->_lcs_pos($y);
324                        $ymids[$k] = $ymids[$k - 1];
325                        break;
326                    }
327                }
328
329                // no reset() here
330                while ($y = current($matches))
331                {
332                    next($matches);
333                    if ($y > $this->seq[$k - 1])
334                    {
335                        // Optimization: this is a common case: next match is just replacing previous match.
336                        $this->in_seq[$this->seq[$k]] = false;
337                        $this->seq[$k] = $y;
338                        $this->in_seq[$y] = 1;
339                    }
340                    else if (empty($this->in_seq[$y]))
341                    {
342                        $k = $this->_lcs_pos($y);
343                        $ymids[$k] = $ymids[$k - 1];
344                    }
345                }
346            }
347        }
348
349        $seps[] = $flip ? array($yoff, $xoff) : array($xoff, $yoff);
350        $ymid = $ymids[$this->lcs];
351
352        for ($n = 0; $n < $nchunks - 1; $n++)
353        {
354            $x1 = $xoff + (int)(($numer + ($xlim - $xoff) * $n) / $nchunks);
355            $y1 = $ymid[$n] + 1;
356            $seps[] = $flip ? array($y1, $x1) : array($x1, $y1);
357        }
358        $seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim);
359
360        return array($this->lcs, $seps);
361    }
362
363    function _lcs_pos($ypos)
364    {
365        $end = $this->lcs;
366
367        if ($end == 0 || $ypos > $this->seq[$end])
368        {
369            $this->seq[++$this->lcs] = $ypos;
370            $this->in_seq[$ypos] = 1;
371            return $this->lcs;
372        }
373
374        $beg = 1;
375        while ($beg < $end)
376        {
377            $mid = (int)(($beg + $end) / 2);
378            if ($ypos > $this->seq[$mid])
379            {
380                $beg = $mid + 1;
381            }
382            else
383            {
384                $end = $mid;
385            }
386        }
387
388        $this->in_seq[$this->seq[$end]] = false;
389        $this->seq[$end] = $ypos;
390        $this->in_seq[$ypos] = 1;
391
392        return $end;
393    }
394
395    /**
396    * Finds LCS of two sequences.
397    *
398    * The results are recorded in the vectors $this->{x,y}changed[], by
399    * storing a 1 in the element for each line that is an insertion or
400    * deletion (ie. is not in the LCS).
401    *
402    * The subsequence of file 0 is (XOFF, XLIM) and likewise for file 1.
403    *
404    * Note that XLIM, YLIM are exclusive bounds.  All line numbers are
405    * origin-0 and discarded lines are not counted.
406    */
407    function _compareseq($xoff, $xlim, $yoff, $ylim)
408    {
409        // Slide down the bottom initial diagonal.
410        while ($xoff < $xlim && $yoff < $ylim && $this->xv[$xoff] == $this->yv[$yoff])
411        {
412            ++$xoff;
413            ++$yoff;
414        }
415
416        // Slide up the top initial diagonal.
417        while ($xlim > $xoff && $ylim > $yoff && $this->xv[$xlim - 1] == $this->yv[$ylim - 1])
418        {
419            --$xlim;
420            --$ylim;
421        }
422
423        if ($xoff == $xlim || $yoff == $ylim)
424        {
425            $lcs = 0;
426        }
427        else
428        {
429            // This is ad hoc but seems to work well.
430            // $nchunks = sqrt(min($xlim - $xoff, $ylim - $yoff) / 2.5);
431            // $nchunks = max(2,min(8,(int)$nchunks));
432            $nchunks = min(7, $xlim - $xoff, $ylim - $yoff) + 1;
433            list($lcs, $seps) = $this->_diag($xoff, $xlim, $yoff, $ylim, $nchunks);
434        }
435
436        if ($lcs == 0)
437        {
438            // X and Y sequences have no common subsequence: mark all changed.
439            while ($yoff < $ylim)
440            {
441                $this->ychanged[$this->yind[$yoff++]] = 1;
442            }
443
444            while ($xoff < $xlim)
445            {
446                $this->xchanged[$this->xind[$xoff++]] = 1;
447            }
448        }
449        else
450        {
451            // Use the partitions to split this problem into subproblems.
452            reset($seps);
453            $pt1 = $seps[0];
454
455            while ($pt2 = next($seps))
456            {
457                $this->_compareseq($pt1[0], $pt2[0], $pt1[1], $pt2[1]);
458                $pt1 = $pt2;
459            }
460        }
461    }
462
463    /**
464    * Adjusts inserts/deletes of identical lines to join changes as much as possible.
465    *
466    * We do something when a run of changed lines include a line at one end
467    * and has an excluded, identical line at the other.  We are free to
468    * choose which identical line is included. 'compareseq' usually chooses
469    * the one at the beginning, but usually it is cleaner to consider the
470    * following identical line to be the "change".
471    *
472    * This is extracted verbatim from analyze.c (GNU diffutils-2.7).
473    */
474    function _shift_boundaries($lines, &$changed, $other_changed)
475    {
476        $i = 0;
477        $j = 0;
478
479        $len = count($lines);
480        $other_len = count($other_changed);
481
482        while (1)
483        {
484            // Scan forward to find the beginning of another run of
485            // changes. Also keep track of the corresponding point in the other file.
486            //
487            // Throughout this code, $i and $j are adjusted together so that
488            // the first $i elements of $changed and the first $j elements of
489            // $other_changed both contain the same number of zeros (unchanged lines).
490            //
491            // Furthermore, $j is always kept so that $j == $other_len or $other_changed[$j] == false.
492            while ($j < $other_len && $other_changed[$j])
493            {
494                $j++;
495            }
496
497            while ($i < $len && ! $changed[$i])
498            {
499                $i++;
500                $j++;
501
502                while ($j < $other_len && $other_changed[$j])
503                {
504                    $j++;
505                }
506            }
507
508            if ($i == $len)
509            {
510                break;
511            }
512
513            $start = $i;
514
515            // Find the end of this run of changes.
516            while (++$i < $len && $changed[$i])
517            {
518                continue;
519            }
520
521            do
522            {
523                // Record the length of this run of changes, so that we can later determine whether the run has grown.
524                $runlength = $i - $start;
525
526                // Move the changed region back, so long as the previous unchanged line matches the last changed one.
527                // This merges with previous changed regions.
528                while ($start > 0 && $lines[$start - 1] == $lines[$i - 1])
529                {
530                    $changed[--$start] = 1;
531                    $changed[--$i] = false;
532
533                    while ($start > 0 && $changed[$start - 1])
534                    {
535                        $start--;
536                    }
537
538                    while ($other_changed[--$j])
539                    {
540                        continue;
541                    }
542                }
543
544                // Set CORRESPONDING to the end of the changed run, at the last point where it corresponds to a changed run in the
545                // other file. CORRESPONDING == LEN means no such point has been found.
546                $corresponding = $j < $other_len ? $i : $len;
547
548                // Move the changed region forward, so long as the first changed line matches the following unchanged one.
549                // This merges with following changed regions.
550                // Do this second, so that if there are no merges, the changed region is moved forward as far as possible.
551                while ($i < $len && $lines[$start] == $lines[$i])
552                {
553                    $changed[$start++] = false;
554                    $changed[$i++] = 1;
555
556                    while ($i < $len && $changed[$i])
557                    {
558                        $i++;
559                    }
560
561                    $j++;
562                    if ($j < $other_len && $other_changed[$j])
563                    {
564                        $corresponding = $i;
565                        while ($j < $other_len && $other_changed[$j])
566                        {
567                            $j++;
568                        }
569                    }
570                }
571            }
572            while ($runlength != $i - $start);
573
574            // If possible, move the fully-merged run of changes back to a corresponding run in the other file.
575            while ($corresponding < $i)
576            {
577                $changed[--$start] = 1;
578                $changed[--$i] = 0;
579
580                while ($other_changed[--$j])
581                {
582                    continue;
583                }
584            }
585        }
586    }
587}