@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
at upstream/main 218 lines 6.2 kB view raw
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}