Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/diffusion/protocol/DiffusionGitUploadPackWireProtocol.php
12242 views
1
<?php
2
3
final 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
}
356
357