xref: /web-bugs/src/Horde/Text/Diff/Engine/String.php (revision e3c4b0ac)
1<?php
2/**
3 * Parses unified or context diffs output from eg. the diff utility.
4 *
5 * Example:
6 * <code>
7 * $patch = file_get_contents('example.patch');
8 * $diff = new Horde_Text_Diff('string', array($patch));
9 * $renderer = new Horde_Text_Diff_Renderer_inline();
10 * echo $renderer->render($diff);
11 * </code>
12 *
13 * Copyright 2005 Örjan Persson <o@42mm.org>
14 * Copyright 2005-2017 Horde LLC (http://www.horde.org/)
15 *
16 * See the enclosed file COPYING for license information (LGPL). If you did
17 * not receive this file, see http://www.horde.org/licenses/lgpl21.
18 *
19 * @author  Örjan Persson <o@42mm.org>
20 * @package Text_Diff
21 */
22class Horde_Text_Diff_Engine_String
23{
24    /**
25     * Parses a unified or context diff.
26     *
27     * First param contains the whole diff and the second can be used to force
28     * a specific diff type. If the second parameter is 'autodetect', the
29     * diff will be examined to find out which type of diff this is.
30     *
31     * @param string $diff  The diff content.
32     * @param string $mode  The diff mode of the content in $diff. One of
33     *                      'context', 'unified', or 'autodetect'.
34     *
35     * @return array  List of all diff operations.
36     * @throws Horde_Text_Diff_Exception
37     */
38    public function diff($diff, $mode = 'autodetect')
39    {
40        // Detect line breaks.
41        $lnbr = "\n";
42        if (strpos($diff, "\r\n") !== false) {
43            $lnbr = "\r\n";
44        } elseif (strpos($diff, "\r") !== false) {
45            $lnbr = "\r";
46        }
47
48        // Make sure we have a line break at the EOF.
49        if (substr($diff, -strlen($lnbr)) != $lnbr) {
50            $diff .= $lnbr;
51        }
52
53        if ($mode != 'autodetect' && $mode != 'context' && $mode != 'unified') {
54            throw new Horde_Text_Diff_Exception('Type of diff is unsupported');
55        }
56
57        if ($mode == 'autodetect') {
58            $context = strpos($diff, '***');
59            $unified = strpos($diff, '---');
60            if ($context === $unified) {
61                throw new Horde_Text_Diff_Exception('Type of diff could not be detected');
62            } elseif ($context === false || $unified === false) {
63                $mode = $context !== false ? 'context' : 'unified';
64            } else {
65                $mode = $context < $unified ? 'context' : 'unified';
66            }
67        }
68
69        // Split by new line and remove the diff header, if there is one.
70        $diff = explode($lnbr, $diff);
71        if (($mode == 'context' && strpos($diff[0], '***') === 0) ||
72            ($mode == 'unified' && strpos($diff[0], '---') === 0)) {
73            array_shift($diff);
74            array_shift($diff);
75        }
76
77        if ($mode == 'context') {
78            return $this->parseContextDiff($diff);
79        } else {
80            return $this->parseUnifiedDiff($diff);
81        }
82    }
83
84    /**
85     * Parses an array containing the unified diff.
86     *
87     * @param array $diff  Array of lines.
88     *
89     * @return array  List of all diff operations.
90     */
91    public function parseUnifiedDiff($diff)
92    {
93        $edits = array();
94        $end = count($diff) - 1;
95        for ($i = 0; $i < $end;) {
96            $diff1 = array();
97            switch (substr($diff[$i], 0, 1)) {
98            case ' ':
99                do {
100                    $diff1[] = substr($diff[$i], 1);
101                } while (++$i < $end && substr($diff[$i], 0, 1) == ' ');
102                $edits[] = new Horde_Text_Diff_Op_Copy($diff1);
103                break;
104
105            case '+':
106                // get all new lines
107                do {
108                    $diff1[] = substr($diff[$i], 1);
109                } while (++$i < $end && substr($diff[$i], 0, 1) == '+');
110                $edits[] = new Horde_Text_Diff_Op_Add($diff1);
111                break;
112
113            case '-':
114                // get changed or removed lines
115                $diff2 = array();
116                do {
117                    $diff1[] = substr($diff[$i], 1);
118                } while (++$i < $end && substr($diff[$i], 0, 1) == '-');
119
120                while ($i < $end && substr($diff[$i], 0, 1) == '+') {
121                    $diff2[] = substr($diff[$i++], 1);
122                }
123                if (count($diff2) == 0) {
124                    $edits[] = new Horde_Text_Diff_Op_Delete($diff1);
125                } else {
126                    $edits[] = new Horde_Text_Diff_Op_Change($diff1, $diff2);
127                }
128                break;
129
130            default:
131                $i++;
132                break;
133            }
134        }
135
136        return $edits;
137    }
138
139    /**
140     * Parses an array containing the context diff.
141     *
142     * @param array $diff  Array of lines.
143     *
144     * @return array  List of all diff operations.
145     */
146    public function parseContextDiff(&$diff)
147    {
148        $edits = array();
149        $i = $max_i = $j = $max_j = 0;
150        $end = count($diff) - 1;
151        while ($i < $end && $j < $end) {
152            while ($i >= $max_i && $j >= $max_j) {
153                // Find the boundaries of the diff output of the two files
154                for ($i = $j;
155                     $i < $end && substr($diff[$i], 0, 3) == '***';
156                     $i++);
157                for ($max_i = $i;
158                     $max_i < $end && substr($diff[$max_i], 0, 3) != '---';
159                     $max_i++);
160                for ($j = $max_i;
161                     $j < $end && substr($diff[$j], 0, 3) == '---';
162                     $j++);
163                for ($max_j = $j;
164                     $max_j < $end && substr($diff[$max_j], 0, 3) != '***';
165                     $max_j++);
166            }
167
168            // find what hasn't been changed
169            $array = array();
170            while ($i < $max_i &&
171                   $j < $max_j &&
172                   strcmp($diff[$i], $diff[$j]) == 0) {
173                $array[] = substr($diff[$i], 2);
174                $i++;
175                $j++;
176            }
177
178            while ($i < $max_i && ($max_j-$j) <= 1) {
179                if ($diff[$i] != '' && substr($diff[$i], 0, 1) != ' ') {
180                    break;
181                }
182                $array[] = substr($diff[$i++], 2);
183            }
184
185            while ($j < $max_j && ($max_i-$i) <= 1) {
186                if ($diff[$j] != '' && substr($diff[$j], 0, 1) != ' ') {
187                    break;
188                }
189                $array[] = substr($diff[$j++], 2);
190            }
191            if (count($array) > 0) {
192                $edits[] = new Horde_Text_Diff_Op_Copy($array);
193            }
194
195            if ($i < $max_i) {
196                $diff1 = array();
197                switch (substr($diff[$i], 0, 1)) {
198                case '!':
199                    $diff2 = array();
200                    do {
201                        $diff1[] = substr($diff[$i], 2);
202                        if ($j < $max_j && substr($diff[$j], 0, 1) == '!') {
203                            $diff2[] = substr($diff[$j++], 2);
204                        }
205                    } while (++$i < $max_i && substr($diff[$i], 0, 1) == '!');
206                    $edits[] = new Horde_Text_Diff_Op_Change($diff1, $diff2);
207                    break;
208
209                case '+':
210                    do {
211                        $diff1[] = substr($diff[$i], 2);
212                    } while (++$i < $max_i && substr($diff[$i], 0, 1) == '+');
213                    $edits[] = new Horde_Text_Diff_Op_Add($diff1);
214                    break;
215
216                case '-':
217                    do {
218                        $diff1[] = substr($diff[$i], 2);
219                    } while (++$i < $max_i && substr($diff[$i], 0, 1) == '-');
220                    $edits[] = new Horde_Text_Diff_Op_Delete($diff1);
221                    break;
222                }
223            }
224
225            if ($j < $max_j) {
226                $diff2 = array();
227                switch (substr($diff[$j], 0, 1)) {
228                case '+':
229                    do {
230                        $diff2[] = substr($diff[$j++], 2);
231                    } while ($j < $max_j && substr($diff[$j], 0, 1) == '+');
232                    $edits[] = new Horde_Text_Diff_Op_Add($diff2);
233                    break;
234
235                case '-':
236                    do {
237                        $diff2[] = substr($diff[$j++], 2);
238                    } while ($j < $max_j && substr($diff[$j], 0, 1) == '-');
239                    $edits[] = new Horde_Text_Diff_Op_Delete($diff2);
240                    break;
241                }
242            }
243        }
244
245        return $edits;
246    }
247}
248