1<?php
2/**
3 * Class used internally by Diff to actually compute the diffs.
4 *
5 * This class uses the Unix `diff` program via shell_exec to compute the
6 * differences between the two input arrays.
7 *
8 * Copyright 2007-2017 Horde LLC (http://www.horde.org/)
9 *
10 * See the enclosed file COPYING for license information (LGPL). If you did
11 * not receive this file, see http://www.horde.org/licenses/lgpl21.
12 *
13 * @author  Milian Wolff <mail@milianw.de>
14 * @package Text_Diff
15 */
16class Horde_Text_Diff_Engine_Shell
17{
18    /**
19     * Path to the diff executable
20     *
21     * @var string
22     */
23    protected $_diffCommand = 'diff';
24
25    /**
26     * Returns the array of differences.
27     *
28     * @param array $from_lines lines of text from old file
29     * @param array $to_lines   lines of text from new file
30     *
31     * @return array all changes made (array with Horde_Text_Diff_Op_* objects)
32     */
33    public function diff($from_lines, $to_lines)
34    {
35        array_walk($from_lines, array('Horde_Text_Diff', 'trimNewlines'));
36        array_walk($to_lines, array('Horde_Text_Diff', 'trimNewlines'));
37
38        // Execute gnu diff or similar to get a standard diff file.
39        $from_file = Horde_Util::getTempFile('Horde_Text_Diff');
40        $to_file = Horde_Util::getTempFile('Horde_Text_Diff');
41        $fp = fopen($from_file, 'w');
42        fwrite($fp, implode("\n", $from_lines));
43        fclose($fp);
44        $fp = fopen($to_file, 'w');
45        fwrite($fp, implode("\n", $to_lines));
46        fclose($fp);
47        $diff = shell_exec($this->_diffCommand . ' ' . $from_file . ' ' . $to_file);
48        unlink($from_file);
49        unlink($to_file);
50
51        if (is_null($diff)) {
52            // No changes were made
53            return array(new Horde_Text_Diff_Op_Copy($from_lines));
54        }
55
56        $from_line_no = 1;
57        $to_line_no = 1;
58        $edits = array();
59
60        // Get changed lines by parsing something like:
61        // 0a1,2
62        // 1,2c4,6
63        // 1,5d6
64        preg_match_all('#^(\d+)(?:,(\d+))?([adc])(\d+)(?:,(\d+))?$#m', $diff,
65            $matches, PREG_SET_ORDER);
66
67        foreach ($matches as $match) {
68            if (!isset($match[5])) {
69                // This paren is not set every time (see regex).
70                $match[5] = false;
71            }
72
73            if ($match[3] == 'a') {
74                $from_line_no--;
75            }
76
77            if ($match[3] == 'd') {
78                $to_line_no--;
79            }
80
81            if ($from_line_no < $match[1] || $to_line_no < $match[4]) {
82                // copied lines
83                assert($match[1] - $from_line_no == $match[4] - $to_line_no);
84                $edits[] =
85                    new Horde_Text_Diff_Op_Copy(
86                        $this->_getLines($from_lines, $from_line_no, $match[1] - 1),
87                        $this->_getLines($to_lines, $to_line_no, $match[4] - 1));
88            }
89
90            switch ($match[3]) {
91            case 'd':
92                // deleted lines
93                $edits[] =
94                    new Horde_Text_Diff_Op_Delete(
95                        $this->_getLines($from_lines, $from_line_no, $match[2]));
96                $to_line_no++;
97                break;
98
99            case 'c':
100                // changed lines
101                $edits[] =
102                    new Horde_Text_Diff_Op_Change(
103                        $this->_getLines($from_lines, $from_line_no, $match[2]),
104                        $this->_getLines($to_lines, $to_line_no, $match[5]));
105                break;
106
107            case 'a':
108                // added lines
109                $edits[] =
110                    new Horde_Text_Diff_Op_Add(
111                        $this->_getLines($to_lines, $to_line_no, $match[5]));
112                $from_line_no++;
113                break;
114            }
115        }
116
117        if (!empty($from_lines)) {
118            // Some lines might still be pending. Add them as copied
119            $edits[] =
120                new Horde_Text_Diff_Op_Copy(
121                    $this->_getLines($from_lines, $from_line_no,
122                                     $from_line_no + count($from_lines) - 1),
123                    $this->_getLines($to_lines, $to_line_no,
124                                     $to_line_no + count($to_lines) - 1));
125        }
126
127        return $edits;
128    }
129
130    /**
131     * Get lines from either the old or new text
132     *
133     * @access private
134     *
135     * @param array &$text_lines Either $from_lines or $to_lines
136     * @param int   &$line_no    Current line number
137     * @param int   $end         Optional end line, when we want to chop more
138     *                           than one line.
139     *
140     * @return array The chopped lines
141     */
142    protected function _getLines(&$text_lines, &$line_no, $end = false)
143    {
144        if (!empty($end)) {
145            $lines = array();
146            // We can shift even more
147            while ($line_no <= $end) {
148                $lines[] = array_shift($text_lines);
149                $line_no++;
150            }
151        } else {
152            $lines = array(array_shift($text_lines));
153            $line_no++;
154        }
155
156        return $lines;
157    }
158}
159