Run update-copyright.py.
[pyassuan.git] / pyassuan / common.py
1 # Copyright (C) 2012 W. Trevor King <wking@drexel.edu>
2 #
3 # This file is part of pyassuan.
4 #
5 # pyassuan is free software: you can redistribute it and/or modify it under the
6 # terms of the GNU General Public License as published by the Free Software
7 # Foundation, either version 3 of the License, or (at your option) any later
8 # version.
9 #
10 # pyassuan is distributed in the hope that it will be useful, but WITHOUT ANY
11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License along with
15 # pyassuan.  If not, see <http://www.gnu.org/licenses/>.
16
17 """Items common to both the client and server
18 """
19
20 import re as _re
21
22 from . import error as _error
23
24
25 LINE_LENGTH = 1002  # 1000 + [CR,]LF
26 _ENCODE_REGEXP = _re.compile(
27     '(' + '|'.join(['%', '\r', '\n']) + ')')
28 _DECODE_REGEXP = _re.compile('(%[0-9A-F]{2})')
29 _REQUEST_REGEXP = _re.compile('^(\w+)( *)(.*)\Z')
30
31
32 def encode(string):
33     r"""
34
35     >>> encode('It grew by 5%!\n')
36     'It grew by 5%25!%0A'
37     """   
38     return _ENCODE_REGEXP.sub(
39         lambda x : to_hex(x.group()), string)
40
41 def decode(string):
42     r"""
43
44     >>> decode('%22Look out!%22%0AWhere%3F')
45     '"Look out!"\nWhere?'
46     """
47     return _DECODE_REGEXP.sub(
48         lambda x : from_hex(x.group()), string)
49
50 def from_hex(code):
51     r"""
52
53     >>> from_hex('%22')
54     '"'
55     >>> from_hex('%0A')
56     '\n'
57     """
58     return chr(int(code[1:], 16))
59
60 def to_hex(char):
61     r"""
62
63     >>> to_hex('"')
64     '%22'
65     >>> to_hex('\n')
66     '%0A'
67     """
68     return '%{:02X}'.format(ord(char))
69
70
71 class Request (object):
72     """A client request
73
74     http://www.gnupg.org/documentation/manuals/assuan/Client-requests.html
75
76     >>> r = Request(command='BYE')
77     >>> str(r)
78     'BYE'
79     >>> r = Request(command='OPTION', parameters='testing at 5%')
80     >>> str(r)
81     'OPTION testing at 5%25'
82     >>> r.from_string('BYE')
83     >>> r.command
84     'BYE'
85     >>> print(r.parameters)
86     None
87     >>> r.from_string('OPTION testing at 5%25')
88     >>> r.command
89     'OPTION'
90     >>> print(r.parameters)
91     testing at 5%
92     >>> r.from_string(' invalid')
93     Traceback (most recent call last):
94       ...
95     pyassuan.error.AssuanError: 170 Invalid request
96     >>> r.from_string('in-valid')
97     Traceback (most recent call last):
98       ...
99     pyassuan.error.AssuanError: 170 Invalid request
100     """
101     def __init__(self, command=None, parameters=None, encoded=False):
102         self.command = command
103         self.parameters = parameters
104         self.encoded = encoded
105
106     def __str__(self):
107         if self.parameters:
108             if self.encoded:
109                 encoded_parameters = self.parameters
110             else:
111                 encoded_parameters = encode(self.parameters)
112             return '{} {}'.format(self.command, encoded_parameters)
113         return self.command
114
115     def from_string(self, string):
116         if len(string) > 1000:  # TODO: byte-vs-str and newlines?
117             raise _error.AssuanError(message='Line too long')
118         match = _REQUEST_REGEXP.match(string)
119         if not match:
120             raise _error.AssuanError(message='Invalid request')
121         self.command = match.group(1)
122         if match.group(3):
123             if match.group(2):
124                 self.parameters = decode(match.group(3))
125             else:
126                 raise _error.AssuanError(message='Invalid request')
127         else:
128             self.parameters = None
129
130
131 class Response (object):
132     """A server response
133
134     http://www.gnupg.org/documentation/manuals/assuan/Server-responses.html
135
136     >>> r = Response(type='OK')
137     >>> str(r)
138     'OK'
139     >>> r = Response(type='ERR', parameters='1 General error')
140     >>> str(r)
141     'ERR 1 General error'
142     >>> r.from_string('OK')
143     >>> r.type
144     'OK'
145     >>> print(r.parameters)
146     None
147     >>> r.from_string('ERR 1 General error')
148     >>> r.type
149     'ERR'
150     >>> print(r.parameters)
151     1 General error
152     >>> r.from_string(' invalid')
153     Traceback (most recent call last):
154       ...
155     pyassuan.error.AssuanError: 76 Invalid response
156     >>> r.from_string('in-valid')
157     Traceback (most recent call last):
158       ...
159     pyassuan.error.AssuanError: 76 Invalid response
160     """
161     types = {
162         'O': 'OK',
163         'E': 'ERR',
164         'S': 'S',
165         '#': '#',
166         'D': 'D',
167         'I': 'INQUIRE',
168         }
169
170     def __init__(self, type=None, parameters=None):
171         self.type = type
172         self.parameters = parameters
173
174     def __str__(self):
175         if self.parameters:
176             return '{} {}'.format(self.type, encode(self.parameters))
177         return self.type
178
179     def from_string(self, string):
180         if len(string) > 1000:  # TODO: byte-vs-str and newlines?
181             raise _error.AssuanError(message='Line too long')
182         try:
183             type = self.types[string[0]]
184         except KeyError:
185             raise _error.AssuanError(message='Invalid response')
186         self.type = type
187         if type == 'D':  # data
188             self.parameters = decode(string[2:])
189         elif type == '#':  # comment
190             self.parameters = decode(string[2:])
191         else:
192             match = _REQUEST_REGEXP.match(string)
193             if not match:
194                 raise _error.AssuanError(message='Invalid request')
195             if match.group(3):
196                 if match.group(2):
197                     self.parameters = decode(match.group(3))
198                 else:
199                     raise _error.AssuanError(message='Invalid request')
200             else:
201                 self.parameters = None
202
203
204 def error_response(error):
205     """
206
207     >>> from pyassuan.error import AssuanError
208     >>> error = AssuanError(1)
209     >>> response = error_response(error)
210     >>> print(response)
211     ERR 1 General error
212     """
213     return Response(type='ERR', parameters=str(error))