7b339281cbab73e6c210ec4f82151b8e439c35c0
[jelmer/dulwich-libgit2.git] / dulwich / protocol.py
1 # protocol.py -- Shared parts of the git protocols
2 # Copryight (C) 2008 John Carr <john.carr@unrouted.co.uk>
3 # Copyright (C) 2008 Jelmer Vernooij <jelmer@samba.org>
4 #
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; version 2
8 # or (at your option) any later version of the License.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
18 # MA  02110-1301, USA.
19
20 """Generic functions for talking the git smart server protocol."""
21
22 from cStringIO import StringIO
23 import os
24 import socket
25
26 from dulwich.errors import (
27     HangupException,
28     GitProtocolError,
29     )
30 from dulwich.misc import (
31     SEEK_END,
32     )
33
34 TCP_GIT_PORT = 9418
35
36 ZERO_SHA = "0" * 40
37
38 SINGLE_ACK = 0
39 MULTI_ACK = 1
40 MULTI_ACK_DETAILED = 2
41
42
43 class ProtocolFile(object):
44     """
45     Some network ops are like file ops. The file ops expect to operate on
46     file objects, so provide them with a dummy file.
47     """
48
49     def __init__(self, read, write):
50         self.read = read
51         self.write = write
52
53     def tell(self):
54         pass
55
56     def close(self):
57         pass
58
59
60 class Protocol(object):
61
62     def __init__(self, read, write, report_activity=None):
63         self.read = read
64         self.write = write
65         self.report_activity = report_activity
66
67     def read_pkt_line(self):
68         """
69         Reads a 'pkt line' from the remote git process
70
71         :return: The next string from the stream
72         """
73         try:
74             sizestr = self.read(4)
75             if not sizestr:
76                 raise HangupException()
77             size = int(sizestr, 16)
78             if size == 0:
79                 if self.report_activity:
80                     self.report_activity(4, 'read')
81                 return None
82             if self.report_activity:
83                 self.report_activity(size, 'read')
84             return self.read(size-4)
85         except socket.error, e:
86             raise GitProtocolError(e)
87
88     def read_pkt_seq(self):
89         pkt = self.read_pkt_line()
90         while pkt:
91             yield pkt
92             pkt = self.read_pkt_line()
93
94     def write_pkt_line(self, line):
95         """
96         Sends a 'pkt line' to the remote git process
97
98         :param line: A string containing the data to send
99         """
100         try:
101             if line is None:
102                 self.write("0000")
103                 if self.report_activity:
104                     self.report_activity(4, 'write')
105             else:
106                 self.write("%04x%s" % (len(line)+4, line))
107                 if self.report_activity:
108                     self.report_activity(4+len(line), 'write')
109         except socket.error, e:
110             raise GitProtocolError(e)
111
112     def write_file(self):
113         class ProtocolFile(object):
114
115             def __init__(self, proto):
116                 self._proto = proto
117                 self._offset = 0
118
119             def write(self, data):
120                 self._proto.write(data)
121                 self._offset += len(data)
122
123             def tell(self):
124                 return self._offset
125
126             def close(self):
127                 pass
128
129         return ProtocolFile(self)
130
131     def write_sideband(self, channel, blob):
132         """
133         Write data to the sideband (a git multiplexing method)
134
135         :param channel: int specifying which channel to write to
136         :param blob: a blob of data (as a string) to send on this channel
137         """
138         # a pktline can be a max of 65520. a sideband line can therefore be
139         # 65520-5 = 65515
140         # WTF: Why have the len in ASCII, but the channel in binary.
141         while blob:
142             self.write_pkt_line("%s%s" % (chr(channel), blob[:65515]))
143             blob = blob[65515:]
144
145     def send_cmd(self, cmd, *args):
146         """
147         Send a command and some arguments to a git server
148
149         Only used for git://
150
151         :param cmd: The remote service to access
152         :param args: List of arguments to send to remove service
153         """
154         self.write_pkt_line("%s %s" % (cmd, "".join(["%s\0" % a for a in args])))
155
156     def read_cmd(self):
157         """
158         Read a command and some arguments from the git client
159
160         Only used for git://
161
162         :return: A tuple of (command, [list of arguments])
163         """
164         line = self.read_pkt_line()
165         splice_at = line.find(" ")
166         cmd, args = line[:splice_at], line[splice_at+1:]
167         assert args[-1] == "\x00"
168         return cmd, args[:-1].split(chr(0))
169
170
171 _RBUFSIZE = 8192  # Default read buffer size.
172
173
174 class ReceivableProtocol(Protocol):
175     """Variant of Protocol that allows reading up to a size without blocking.
176
177     This class has a recv() method that behaves like socket.recv() in addition
178     to a read() method.
179
180     If you want to read n bytes from the wire and block until exactly n bytes
181     (or EOF) are read, use read(n). If you want to read at most n bytes from the
182     wire but don't care if you get less, use recv(n). Note that recv(n) will
183     still block until at least one byte is read.
184     """
185
186     def __init__(self, recv, write, report_activity=None, rbufsize=_RBUFSIZE):
187         super(ReceivableProtocol, self).__init__(self.read, write,
188                                                  report_activity)
189         self._recv = recv
190         self._rbuf = StringIO()
191         self._rbufsize = rbufsize
192
193     def read(self, size):
194         # From _fileobj.read in socket.py in the Python 2.6.5 standard library,
195         # with the following modifications:
196         #  - omit the size <= 0 branch
197         #  - seek back to start rather than 0 in case some buffer has been
198         #    consumed.
199         #  - use SEEK_END instead of the magic number.
200         # Copyright (c) 2001-2010 Python Software Foundation; All Rights Reserved
201         # Licensed under the Python Software Foundation License.
202         # TODO: see if buffer is more efficient than cStringIO.
203         assert size > 0
204
205         # Our use of StringIO rather than lists of string objects returned by
206         # recv() minimizes memory usage and fragmentation that occurs when
207         # rbufsize is large compared to the typical return value of recv().
208         buf = self._rbuf
209         start = buf.tell()
210         buf.seek(0, SEEK_END)
211         # buffer may have been partially consumed by recv()
212         buf_len = buf.tell() - start
213         if buf_len >= size:
214             # Already have size bytes in our buffer?  Extract and return.
215             buf.seek(start)
216             rv = buf.read(size)
217             self._rbuf = StringIO()
218             self._rbuf.write(buf.read())
219             self._rbuf.seek(0)
220             return rv
221
222         self._rbuf = StringIO()  # reset _rbuf.  we consume it via buf.
223         while True:
224             left = size - buf_len
225             # recv() will malloc the amount of memory given as its
226             # parameter even though it often returns much less data
227             # than that.  The returned data string is short lived
228             # as we copy it into a StringIO and free it.  This avoids
229             # fragmentation issues on many platforms.
230             data = self._recv(left)
231             if not data:
232                 break
233             n = len(data)
234             if n == size and not buf_len:
235                 # Shortcut.  Avoid buffer data copies when:
236                 # - We have no data in our buffer.
237                 # AND
238                 # - Our call to recv returned exactly the
239                 #   number of bytes we were asked to read.
240                 return data
241             if n == left:
242                 buf.write(data)
243                 del data  # explicit free
244                 break
245             assert n <= left, "_recv(%d) returned %d bytes" % (left, n)
246             buf.write(data)
247             buf_len += n
248             del data  # explicit free
249             #assert buf_len == buf.tell()
250         buf.seek(start)
251         return buf.read()
252
253     def recv(self, size):
254         assert size > 0
255
256         buf = self._rbuf
257         start = buf.tell()
258         buf.seek(0, SEEK_END)
259         buf_len = buf.tell()
260         buf.seek(start)
261
262         left = buf_len - start
263         if not left:
264             # only read from the wire if our read buffer is exhausted
265             data = self._recv(self._rbufsize)
266             if len(data) == size:
267                 # shortcut: skip the buffer if we read exactly size bytes
268                 return data
269             buf = StringIO()
270             buf.write(data)
271             buf.seek(0)
272             del data  # explicit free
273             self._rbuf = buf
274         return buf.read(size)
275
276
277 def extract_capabilities(text):
278     """Extract a capabilities list from a string, if present.
279
280     :param text: String to extract from
281     :return: Tuple with text with capabilities removed and list of capabilities
282     """
283     if not "\0" in text:
284         return text, []
285     text, capabilities = text.rstrip().split("\0")
286     return (text, capabilities.strip().split(" "))
287
288
289 def extract_want_line_capabilities(text):
290     """Extract a capabilities list from a want line, if present.
291
292     Note that want lines have capabilities separated from the rest of the line
293     by a space instead of a null byte. Thus want lines have the form:
294
295         want obj-id cap1 cap2 ...
296
297     :param text: Want line to extract from
298     :return: Tuple with text with capabilities removed and list of capabilities
299     """
300     split_text = text.rstrip().split(" ")
301     if len(split_text) < 3:
302         return text, []
303     return (" ".join(split_text[:2]), split_text[2:])
304
305
306 def ack_type(capabilities):
307     """Extract the ack type from a capabilities list."""
308     if 'multi_ack_detailed' in capabilities:
309         return MULTI_ACK_DETAILED
310     elif 'multi_ack' in capabilities:
311         return MULTI_ACK
312     return SINGLE_ACK