@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3final class DiffusionSubversionWireProtocol extends Phobject {
4
5 private $buffer = '';
6 private $state = 'item';
7 private $expectBytes = 0;
8 private $byteBuffer = '';
9 private $stack = array();
10 private $list = array();
11 private $raw = '';
12
13 private function pushList() {
14 $this->stack[] = $this->list;
15 $this->list = array();
16 }
17
18 private function popList() {
19 $list = $this->list;
20 $this->list = array_pop($this->stack);
21 return $list;
22 }
23
24 private function pushItem($item, $type) {
25 $this->list[] = array(
26 'type' => $type,
27 'value' => $item,
28 );
29 }
30
31 public function writeData($data) {
32 $this->buffer .= $data;
33
34 $messages = array();
35 while (true) {
36 if ($this->state == 'space') {
37 // Consume zero or more extra spaces after matching an item. The
38 // protocol requires at least one space, but allows more than one.
39
40 $matches = null;
41 if (!preg_match('/^(\s*)\S/', $this->buffer, $matches)) {
42 // Wait for more data.
43 break;
44 }
45
46 // We have zero or more spaces and then some other character, so throw
47 // the spaces away and continue parsing frames.
48 if (strlen($matches[1])) {
49 $this->buffer = substr($this->buffer, strlen($matches[1]));
50 }
51
52 $this->state = 'item';
53 } else if ($this->state == 'item') {
54 $match = null;
55 $result = null;
56 $buf = $this->buffer;
57 if (preg_match('/^([a-z][a-z0-9-]*)\s/i', $buf, $match)) {
58 $this->pushItem($match[1], 'word');
59 } else if (preg_match('/^(\d+)\s/', $buf, $match)) {
60 $this->pushItem((int)$match[1], 'number');
61 } else if (preg_match('/^(\d+):/', $buf, $match)) {
62 // NOTE: The "+ 1" includes the space after the string.
63 $this->expectBytes = (int)$match[1] + 1;
64 $this->state = 'bytes';
65 } else if (preg_match('/^(\\()\s/', $buf, $match)) {
66 $this->pushList();
67 } else if (preg_match('/^(\\))\s/', $buf, $match)) {
68 $list = $this->popList();
69 if ($this->stack) {
70 $this->pushItem($list, 'list');
71 } else {
72 $result = $list;
73 }
74 } else {
75 $match = false;
76 }
77
78 if ($match !== false) {
79 $this->raw .= substr($this->buffer, 0, strlen($match[0]));
80 $this->buffer = substr($this->buffer, strlen($match[0]));
81
82 if ($result !== null) {
83 $messages[] = array(
84 'structure' => $list,
85 'raw' => $this->raw,
86 );
87 $this->raw = '';
88 }
89
90 // Consume any extra whitespace after an item. If we're in the
91 // "bytes" state, we aren't looking for whitespace.
92 if ($this->state == 'item') {
93 $this->state = 'space';
94 }
95 } else {
96 // No matches yet, wait for more data.
97 break;
98 }
99 } else if ($this->state == 'bytes') {
100 $new_data = substr($this->buffer, 0, $this->expectBytes);
101 if (!strlen($new_data)) {
102 // No more bytes available yet, wait for more data.
103 break;
104 }
105 $this->buffer = substr($this->buffer, strlen($new_data));
106
107 $this->expectBytes -= strlen($new_data);
108 $this->raw .= $new_data;
109 $this->byteBuffer .= $new_data;
110
111 if (!$this->expectBytes) {
112 $this->state = 'byte-space';
113 // Strip off the terminal space.
114 $this->pushItem(substr($this->byteBuffer, 0, -1), 'string');
115 $this->byteBuffer = '';
116 $this->state = 'space';
117 }
118 } else {
119 throw new Exception(pht("Invalid state '%s'!", $this->state));
120 }
121 }
122
123 return $messages;
124 }
125
126 /**
127 * Convert a parsed command struct into a wire protocol string.
128 */
129 public function serializeStruct(array $struct) {
130 $out = array();
131
132 $out[] = '( ';
133 foreach ($struct as $item) {
134 $value = $item['value'];
135 $type = $item['type'];
136 switch ($type) {
137 case 'number':
138 case 'word':
139 $out[] = $value;
140 break;
141 case 'string':
142 $out[] = strlen($value).':'.$value;
143 break;
144 case 'list':
145 $out[] = self::serializeStruct($value);
146 break;
147 default:
148 throw new Exception(
149 pht(
150 "Unknown SVN wire protocol structure '%s'!",
151 $type));
152 }
153 if ($type != 'list') {
154 $out[] = ' ';
155 }
156 }
157 $out[] = ') ';
158
159 return implode('', $out);
160 }
161
162 public function isReadOnlyCommand(array $struct) {
163 if (empty($struct[0]['type']) || ($struct[0]['type'] != 'word')) {
164 // This isn't what we expect; fail defensively.
165 throw new Exception(
166 pht(
167 "Unexpected command structure, expected '%s'.",
168 '( word ... )'));
169 }
170
171 switch ($struct[0]['value']) {
172 // Authentication command set.
173 case 'EXTERNAL':
174
175 // The "Main" command set. Some of the commands in this command set are
176 // mutation commands, and are omitted from this list.
177 case 'reparent':
178 case 'get-latest-rev':
179 case 'get-dated-rev':
180 case 'rev-proplist':
181 case 'rev-prop':
182 case 'get-file':
183 case 'get-dir':
184 case 'check-path':
185 case 'stat':
186 case 'update':
187 case 'get-mergeinfo':
188 case 'switch':
189 case 'status':
190 case 'diff':
191 case 'log':
192 case 'get-file-revs':
193 case 'get-locations':
194
195 // The "Report" command set. These are not actually mutation
196 // operations, they just define a request for information.
197 case 'set-path':
198 case 'delete-path':
199 case 'link-path':
200 case 'finish-report':
201 case 'abort-report':
202
203 // These are used to report command results.
204 case 'success':
205 case 'failure':
206
207 // If we get here, we've matched some known read-only command.
208 return true;
209 default:
210 // Anything else isn't a known read-only command, so require write
211 // access to use it.
212 break;
213 }
214
215 return false;
216 }
217
218}