@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 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}