@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 355 lines 9.8 kB view raw
1<?php 2 3final class DiffusionGitUploadPackWireProtocol 4 extends DiffusionGitWireProtocol { 5 6 private $readMode = 'length'; 7 private $readBuffer; 8 private $readFrameLength; 9 private $readFrames = array(); 10 11 private $readFrameMode = 'refs'; 12 private $refFrames = array(); 13 14 private $readMessages = array(); 15 16 public function willReadBytes($bytes) { 17 if ($this->readBuffer === null) { 18 $this->readBuffer = new PhutilRope(); 19 } 20 $buffer = $this->readBuffer; 21 22 $buffer->append($bytes); 23 24 while (true) { 25 $len = $buffer->getByteLength(); 26 switch ($this->readMode) { 27 case 'length': 28 // We're expecting 4 bytes containing the length of the protocol 29 // frame as hexadecimal in ASCII text, like "01ab". Wait until we 30 // see at least 4 bytes on the wire. 31 if ($len < 4) { 32 if ($len > 0) { 33 $bytes = $this->peekBytes($len); 34 if (!preg_match('/^[0-9a-f]+\z/', $bytes)) { 35 throw new Exception( 36 pht( 37 'Bad frame length character in Git protocol ("%s"), '. 38 'expected a 4-digit hexadecimal value encoded as ASCII '. 39 'text.', 40 $bytes)); 41 } 42 } 43 44 // We can't make any more progress until we get enough bytes, so 45 // we're done with state processing. 46 break 2; 47 } 48 49 $frame_length = $this->readBytes(4); 50 $frame_length = hexdec($frame_length); 51 52 // Note that the frame length includes the 4 header bytes, so we 53 // usually expect a length of 5 or larger. Frames with length 0 54 // are boundaries. 55 if ($frame_length === 0) { 56 $this->readFrames[] = $this->newProtocolFrame('null', ''); 57 } else if ($frame_length >= 1 && $frame_length <= 3) { 58 throw new Exception( 59 pht( 60 'Encountered Git protocol frame with unexpected frame '. 61 'length (%s)!', 62 $frame_length)); 63 } else { 64 $this->readFrameLength = $frame_length - 4; 65 $this->readMode = 'frame'; 66 } 67 68 break; 69 case 'frame': 70 // We're expecting a protocol frame of a specified length. Note that 71 // it is possible for a frame to have length 0. 72 73 // We don't have enough bytes yet, so wait for more. 74 if ($len < $this->readFrameLength) { 75 break 2; 76 } 77 78 if ($this->readFrameLength > 0) { 79 $bytes = $this->readBytes($this->readFrameLength); 80 } else { 81 $bytes = ''; 82 } 83 84 // Emit a protocol frame. 85 $this->readFrames[] = $this->newProtocolFrame('data', $bytes); 86 $this->readMode = 'length'; 87 break; 88 } 89 } 90 91 while (true) { 92 switch ($this->readFrameMode) { 93 case 'refs': 94 if (!$this->readFrames) { 95 break 2; 96 } 97 98 foreach ($this->readFrames as $key => $frame) { 99 unset($this->readFrames[$key]); 100 101 if ($frame['type'] === 'null') { 102 $ref_frames = $this->refFrames; 103 $this->refFrames = array(); 104 105 $ref_frames[] = $frame; 106 107 $this->readMessages[] = $this->newProtocolRefMessage($ref_frames); 108 $this->readFrameMode = 'passthru'; 109 break; 110 } else { 111 $this->refFrames[] = $frame; 112 } 113 } 114 115 break; 116 case 'passthru': 117 if (!$this->readFrames) { 118 break 2; 119 } 120 121 $this->readMessages[] = $this->newProtocolDataMessage( 122 $this->readFrames); 123 $this->readFrames = array(); 124 125 break; 126 } 127 } 128 129 $wire = array(); 130 foreach ($this->readMessages as $key => $message) { 131 $wire[] = $message; 132 unset($this->readMessages[$key]); 133 } 134 $wire = implode('', $wire); 135 136 return $wire; 137 } 138 139 public function willWriteBytes($bytes) { 140 return $bytes; 141 } 142 143 private function readBytes($count) { 144 $buffer = $this->readBuffer; 145 146 $bytes = $buffer->getPrefixBytes($count); 147 $buffer->removeBytesFromHead($count); 148 149 return $bytes; 150 } 151 152 private function peekBytes($count) { 153 $buffer = $this->readBuffer; 154 return $buffer->getPrefixBytes($count); 155 } 156 157 private function newProtocolFrame($type, $bytes) { 158 return array( 159 'type' => $type, 160 'length' => strlen($bytes), 161 'bytes' => $bytes, 162 ); 163 } 164 165 private function newProtocolRefMessage(array $frames) { 166 $head_key = head_key($frames); 167 $last_key = last_key($frames); 168 169 $capabilities = null; 170 $last_frame = null; 171 172 $refs = array(); 173 foreach ($frames as $key => $frame) { 174 $is_last = ($key === $last_key); 175 if ($is_last) { 176 // This is a "0000" frame at the end of the list of refs, so we pass 177 // it through unmodified after we figure out what the rest of the 178 // frames should look like, below. 179 $last_frame = $frame; 180 continue; 181 } 182 183 $is_first = ($key === $head_key); 184 185 // Otherwise, we expect a list of: 186 // 187 // <hash> <ref-name>\0<capabilities> 188 // <hash> <ref-name> 189 // ... 190 // 191 // See T13309. The end of this list (which may be empty if a repository 192 // does not have any refs) has a list of zero or more of these: 193 // 194 // shallow <hash> 195 // 196 // These entries are present if the repository is a shallow clone 197 // which was made with the "--depth" flag. 198 // 199 // Note that "shallow" frames do not advertise capabilities, and if 200 // a repository has only "shallow" frames, capabilities are never 201 // advertised. 202 203 $bytes = $frame['bytes']; 204 $matches = array(); 205 if ($is_first) { 206 $capabilities_pattern = '\0(?P<capabilities>[^\n]+)'; 207 } else { 208 $capabilities_pattern = ''; 209 } 210 211 $ok = preg_match( 212 '('. 213 '^'. 214 '(?:'. 215 '(?P<hash>[0-9a-f]{40}) (?P<name>[^\0\n]+)'.$capabilities_pattern. 216 '|'. 217 'shallow (?P<shallow>[0-9a-f]{40})'. 218 ')'. 219 '\n'. 220 '\z'. 221 ')', 222 $bytes, 223 $matches); 224 225 if (!$ok) { 226 if ($is_first) { 227 throw new Exception( 228 pht( 229 'Unexpected "git upload-pack" initial protocol frame: expected '. 230 '"<hash> <name>\0<capabilities>\n", or '. 231 '"shallow <hash>\n", got "%s".', 232 $bytes)); 233 } else { 234 throw new Exception( 235 pht( 236 'Unexpected "git upload-pack" protocol frame: expected '. 237 '"<hash> <name>\n", or "shallow <hash>\n", got "%s".', 238 $bytes)); 239 } 240 } 241 242 if (isset($matches['shallow'])) { 243 $name = null; 244 $hash = $matches['shallow']; 245 $is_shallow = true; 246 } else { 247 $name = $matches['name']; 248 $hash = $matches['hash']; 249 $is_shallow = false; 250 } 251 252 if (isset($matches['capabilities'])) { 253 $capabilities = $matches['capabilities']; 254 } 255 256 $refs[] = array( 257 'hash' => $hash, 258 'name' => $name, 259 'shallow' => $is_shallow, 260 ); 261 } 262 263 $capabilities = DiffusionGitWireProtocolCapabilities::newFromWireFormat( 264 $capabilities); 265 266 $ref_list = id(new DiffusionGitWireProtocolRefList()) 267 ->setCapabilities($capabilities); 268 269 foreach ($refs as $ref) { 270 $wire_ref = id(new DiffusionGitWireProtocolRef()) 271 ->setHash($ref['hash']); 272 273 if ($ref['shallow']) { 274 $wire_ref->setIsShallow(true); 275 } else { 276 $wire_ref->setName($ref['name']); 277 } 278 279 $ref_list->addRef($wire_ref); 280 } 281 282 // TODO: Here, we have a structured list of refs. In a future change, 283 // we are free to mutate the structure before flattening it back into 284 // wire format. 285 286 $refs = $ref_list->getRefs(); 287 288 // Before we write the ref list, sort it for consistency with native 289 // Git output. We may have added, removed, or renamed refs and ended up 290 // with an out-of-order list. 291 292 $refs = msortv($refs, 'newSortVector'); 293 294 // The first ref we send back includes the capabilities data. Note that if 295 // we send back no refs, we also don't send back capabilities! This is 296 // a little surprising, but is consistent with the native behavior of the 297 // protocol. 298 299 // Likewise, we don't send back any capabilities if we're sending only 300 // "shallow" frames. 301 302 $output = array(); 303 $is_first = true; 304 foreach ($refs as $ref) { 305 $is_shallow = $ref->getIsShallow(); 306 307 if ($is_shallow) { 308 $result = sprintf( 309 "shallow %s\n", 310 $ref->getHash()); 311 } else if ($is_first) { 312 $result = sprintf( 313 "%s %s\0%s\n", 314 $ref->getHash(), 315 $ref->getName(), 316 $ref_list->getCapabilities()->toWireFormat()); 317 } else { 318 $result = sprintf( 319 "%s %s\n", 320 $ref->getHash(), 321 $ref->getName()); 322 } 323 324 $output[] = $this->newProtocolFrame('data', $result); 325 $is_first = false; 326 } 327 328 $output[] = $last_frame; 329 330 return $this->newProtocolDataMessage($output); 331 } 332 333 private function newProtocolDataMessage(array $frames) { 334 $message = array(); 335 336 foreach ($frames as $frame) { 337 switch ($frame['type']) { 338 case 'null': 339 $message[] = '0000'; 340 break; 341 case 'data': 342 $message[] = sprintf( 343 '%04x%s', 344 $frame['length'] + 4, 345 $frame['bytes']); 346 break; 347 } 348 } 349 350 $message = implode('', $message); 351 352 return $message; 353 } 354 355}