3 final class DiffusionSubversionWireProtocol
extends Phobject
{
6 private $state = 'item';
7 private $expectBytes = 0;
8 private $byteBuffer = '';
9 private $stack = array();
10 private $list = array();
13 private function pushList() {
14 $this->stack
[] = $this->list;
15 $this->list = array();
18 private function popList() {
20 $this->list = array_pop($this->stack
);
24 private function pushItem($item, $type) {
25 $this->list[] = array(
31 public function writeData($data) {
32 $this->buffer
.= $data;
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.
41 if (!preg_match('/^(\s*)\S/', $this->buffer
, $matches)) {
42 // Wait for more data.
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]));
52 $this->state
= 'item';
53 } else if ($this->state
== 'item') {
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)) {
67 } else if (preg_match('/^(\\))\s/', $buf, $match)) {
68 $list = $this->popList();
70 $this->pushItem($list, 'list');
78 if ($match !== false) {
79 $this->raw
.= substr($this->buffer
, 0, strlen($match[0]));
80 $this->buffer
= substr($this->buffer
, strlen($match[0]));
82 if ($result !== null) {
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';
96 // No matches yet, wait for more data.
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.
105 $this->buffer
= substr($this->buffer
, strlen($new_data));
107 $this->expectBytes
-= strlen($new_data);
108 $this->raw
.= $new_data;
109 $this->byteBuffer
.= $new_data;
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';
119 throw new Exception(pht("Invalid state '%s'!", $this->state
));
127 * Convert a parsed command struct into a wire protocol string.
129 public function serializeStruct(array $struct) {
133 foreach ($struct as $item) {
134 $value = $item['value'];
135 $type = $item['type'];
144 $out[] = strlen($value).':'.$value;
147 $out[] = self
::serializeStruct($value);
152 "Unknown SVN wire protocol structure '%s'!",
155 if ($type != 'list') {
161 return implode('', $out);
164 public function isReadOnlyCommand(array $struct) {
165 if (empty($struct[0]['type']) ||
($struct[0]['type'] != 'word')) {
166 // This isn't what we expect; fail defensively.
169 "Unexpected command structure, expected '%s'.",
173 switch ($struct[0]['value']) {
174 // Authentication command set.
177 // The "Main" command set. Some of the commands in this command set are
178 // mutation commands, and are omitted from this list.
180 case 'get-latest-rev':
181 case 'get-dated-rev':
189 case 'get-mergeinfo':
194 case 'get-file-revs':
195 case 'get-locations':
197 // The "Report" command set. These are not actually mutation
198 // operations, they just define a request for information.
202 case 'finish-report':
205 // These are used to report command results.
209 // If we get here, we've matched some known read-only command.
212 // Anything else isn't a known read-only command, so require write