Correct a parameter order swap in "diffusion.historyquery" for Mercurial
[phabricator.git] / src / applications / diffusion / ssh / DiffusionMercurialWireClientSSHProtocolChannel.php
blob8eb026d8fe804357dc7f8dbc0ad9edc3851fe711
1 <?php
3 final class DiffusionMercurialWireClientSSHProtocolChannel
4 extends PhutilProtocolChannel {
6 private $buffer = '';
7 private $state = 'command';
8 private $expectArgumentCount;
9 private $argumentName;
10 private $expectBytes;
11 private $command;
12 private $arguments;
13 private $raw;
15 protected function encodeMessage($message) {
16 return $message;
19 private function initializeState($last_command = null) {
20 if ($last_command == 'unbundle') {
21 $this->command = '<raw-data>';
22 $this->state = 'data-length';
23 } else {
24 $this->state = 'command';
26 $this->expectArgumentCount = null;
27 $this->expectBytes = null;
28 $this->command = null;
29 $this->argumentName = null;
30 $this->arguments = array();
31 $this->raw = '';
34 private function readProtocolLine() {
35 $pos = strpos($this->buffer, "\n");
37 if ($pos === false) {
38 return null;
41 $line = substr($this->buffer, 0, $pos);
43 $this->raw .= $line."\n";
44 $this->buffer = substr($this->buffer, $pos + 1);
46 return $line;
49 private function readProtocolBytes() {
50 if (strlen($this->buffer) < $this->expectBytes) {
51 return null;
54 $bytes = substr($this->buffer, 0, $this->expectBytes);
55 $this->raw .= $bytes;
56 $this->buffer = substr($this->buffer, $this->expectBytes);
58 return $bytes;
61 private function newMessageAndResetState() {
62 $message = array(
63 'command' => $this->command,
64 'arguments' => $this->arguments,
65 'raw' => $this->raw,
67 $this->initializeState($this->command);
68 return $message;
71 private function newDataMessage($bytes) {
72 $message = array(
73 'command' => '<raw-data>',
74 'raw' => strlen($bytes)."\n".$bytes,
76 return $message;
79 protected function decodeStream($data) {
80 $this->buffer .= $data;
82 $out = array();
83 $messages = array();
85 while (true) {
86 if ($this->state == 'command') {
87 $this->initializeState();
89 // We're reading a command. It looks like:
91 // <command>
93 $line = $this->readProtocolLine();
94 if ($line === null) {
95 break;
98 $this->command = $line;
99 $this->state = 'arguments';
100 } else if ($this->state == 'arguments') {
102 // Check if we're still waiting for arguments.
103 $args = DiffusionMercurialWireProtocol::getCommandArgs($this->command);
104 $have = array_select_keys($this->arguments, $args);
105 if (count($have) == count($args)) {
106 // We have all the arguments. Emit a message and read the next
107 // command.
108 $messages[] = $this->newMessageAndResetState();
109 } else {
110 // We're still reading arguments. They can either look like:
112 // <name> <length(value)>
113 // <value>
114 // ...
116 // ...or like this:
118 // * <count>
119 // <name1> <length(value1)>
120 // <value1>
121 // ...
123 $line = $this->readProtocolLine();
124 if ($line === null) {
125 break;
128 list($arg, $size) = explode(' ', $line, 2);
129 $size = (int)$size;
131 if ($arg != '*') {
132 $this->expectBytes = $size;
133 $this->argumentName = $arg;
134 $this->state = 'value';
135 } else {
136 $this->arguments['*'] = array();
137 $this->expectArgumentCount = $size;
138 $this->state = 'argv';
141 } else if ($this->state == 'value' || $this->state == 'argv-value') {
143 // We're reading the value of an argument. We just need to wait for
144 // the right number of bytes to show up.
146 $bytes = $this->readProtocolBytes();
147 if ($bytes === null) {
148 break;
151 if ($this->state == 'argv-value') {
152 $this->arguments['*'][$this->argumentName] = $bytes;
153 $this->state = 'argv';
154 } else {
155 $this->arguments[$this->argumentName] = $bytes;
156 $this->state = 'arguments';
160 } else if ($this->state == 'argv') {
162 // We're reading a variable number of arguments. We need to wait for
163 // the arguments to arrive.
165 if ($this->expectArgumentCount) {
166 $line = $this->readProtocolLine();
167 if ($line === null) {
168 break;
171 list($arg, $size) = explode(' ', $line, 2);
172 $size = (int)$size;
174 $this->expectBytes = $size;
175 $this->argumentName = $arg;
176 $this->state = 'argv-value';
178 $this->expectArgumentCount--;
179 } else {
180 $this->state = 'arguments';
182 } else if ($this->state == 'data-length') {
184 // We're reading the length of a chunk of raw data. It looks like
185 // this:
187 // <length-in-bytes>\n
189 // The length is human-readable text (for example, "4096"), and
190 // may be 0.
192 $line = $this->readProtocolLine();
193 if ($line === null) {
194 break;
196 $this->expectBytes = (int)$line;
197 if (!$this->expectBytes) {
198 $messages[] = $this->newDataMessage('');
199 $this->initializeState();
200 } else {
201 $this->state = 'data-bytes';
203 } else if ($this->state == 'data-bytes') {
205 // We're reading some known, nonzero number of raw bytes of data.
207 // If we don't have any more bytes on the buffer yet, just bail:
208 // otherwise, we'll emit a pointless and possibly harmful 0-byte data
209 // frame. See T13036 for discussion.
210 if (!strlen($this->buffer)) {
211 break;
214 $bytes = substr($this->buffer, 0, $this->expectBytes);
215 $this->buffer = substr($this->buffer, strlen($bytes));
216 $this->expectBytes -= strlen($bytes);
218 // NOTE: We emit a data frame as soon as we read some data. This can
219 // cause us to repackage frames: for example, if we receive one large
220 // frame slowly, we may emit it as several smaller frames. In theory
221 // this is good; in practice, Mercurial never seems to select a frame
222 // size larger than 4096 bytes naturally and this may be more
223 // complexity and trouble than it is worth. See T13036.
225 $messages[] = $this->newDataMessage($bytes);
227 if (!$this->expectBytes) {
228 // We've finished reading this chunk, so go read the next chunk.
229 $this->state = 'data-length';
230 } else {
231 // We're waiting for more data, and have read everything available
232 // to us so far.
233 break;
235 } else {
236 throw new Exception(pht("Bad parser state '%s'!", $this->state));
240 return $messages;