Show More
@@ -660,6 +660,64 b' 0x01' | |||||
660 | 0x02 |
|
660 | 0x02 | |
661 | The error occurred at the application level. e.g. invalid command. |
|
661 | The error occurred at the application level. e.g. invalid command. | |
662 |
|
662 | |||
|
663 | Human Output Side-Channel (``0x06``) | |||
|
664 | ------------------------------------ | |||
|
665 | ||||
|
666 | This frame contains a message that is intended to be displayed to | |||
|
667 | people. Whereas most frames communicate machine readable data, this | |||
|
668 | frame communicates textual data that is intended to be shown to | |||
|
669 | humans. | |||
|
670 | ||||
|
671 | The frame consists of a series of *formatting requests*. Each formatting | |||
|
672 | request consists of a formatting string, arguments for that formatting | |||
|
673 | string, and labels to apply to that formatting string. | |||
|
674 | ||||
|
675 | A formatting string is a printf()-like string that allows variable | |||
|
676 | substitution within the string. Labels allow the rendered text to be | |||
|
677 | *decorated*. Assuming use of the canonical Mercurial code base, a | |||
|
678 | formatting string can be the input to the ``i18n._`` function. This | |||
|
679 | allows messages emitted from the server to be localized. So even if | |||
|
680 | the server has different i18n settings, people could see messages in | |||
|
681 | their *native* settings. Similarly, the use of labels allows | |||
|
682 | decorations like coloring and underlining to be applied using the | |||
|
683 | client's configured rendering settings. | |||
|
684 | ||||
|
685 | Formatting strings are similar to ``printf()`` strings or how | |||
|
686 | Python's ``%`` operator works. The only supported formatting sequences | |||
|
687 | are ``%s`` and ``%%``. ``%s`` will be replaced by whatever the string | |||
|
688 | at that position resolves to. ``%%`` will be replaced by ``%``. All | |||
|
689 | other 2-byte sequences beginning with ``%`` represent a literal | |||
|
690 | ``%`` followed by that character. However, future versions of the | |||
|
691 | wire protocol reserve the right to allow clients to opt in to receiving | |||
|
692 | formatting strings with additional formatters, hence why ``%%`` is | |||
|
693 | required to represent the literal ``%``. | |||
|
694 | ||||
|
695 | The raw frame consists of a series of data structures representing | |||
|
696 | textual atoms to print. Each atom begins with a struct defining the | |||
|
697 | size of the data that follows: | |||
|
698 | ||||
|
699 | * A 16-bit little endian unsigned integer denoting the length of the | |||
|
700 | formatting string. | |||
|
701 | * An 8-bit unsigned integer denoting the number of label strings | |||
|
702 | that follow. | |||
|
703 | * An 8-bit unsigned integer denoting the number of formatting string | |||
|
704 | arguments strings that follow. | |||
|
705 | * An array of 8-bit unsigned integers denoting the lengths of | |||
|
706 | *labels* data. | |||
|
707 | * An array of 16-bit unsigned integers denoting the lengths of | |||
|
708 | formatting strings. | |||
|
709 | * The formatting string, encoded as UTF-8. | |||
|
710 | * 0 or more ASCII strings defining labels to apply to this atom. | |||
|
711 | * 0 or more UTF-8 strings that will be used as arguments to the | |||
|
712 | formatting string. | |||
|
713 | ||||
|
714 | All data to be printed MUST be encoded into a single frame: this frame | |||
|
715 | does not support spanning data across multiple frames. | |||
|
716 | ||||
|
717 | All textual data encoded in these frames is assumed to be line delimited. | |||
|
718 | The last atom in the frame SHOULD end with a newline (``\n``). If it | |||
|
719 | doesn't, clients MAY add a newline to facilitate immediate printing. | |||
|
720 | ||||
663 | Issuing Commands |
|
721 | Issuing Commands | |
664 | ---------------- |
|
722 | ---------------- | |
665 |
|
723 |
@@ -27,6 +27,7 b' FRAME_TYPE_COMMAND_ARGUMENT = 0x02' | |||||
27 | FRAME_TYPE_COMMAND_DATA = 0x03 |
|
27 | FRAME_TYPE_COMMAND_DATA = 0x03 | |
28 | FRAME_TYPE_BYTES_RESPONSE = 0x04 |
|
28 | FRAME_TYPE_BYTES_RESPONSE = 0x04 | |
29 | FRAME_TYPE_ERROR_RESPONSE = 0x05 |
|
29 | FRAME_TYPE_ERROR_RESPONSE = 0x05 | |
|
30 | FRAME_TYPE_TEXT_OUTPUT = 0x06 | |||
30 |
|
31 | |||
31 | FRAME_TYPES = { |
|
32 | FRAME_TYPES = { | |
32 | b'command-name': FRAME_TYPE_COMMAND_NAME, |
|
33 | b'command-name': FRAME_TYPE_COMMAND_NAME, | |
@@ -34,6 +35,7 b' FRAME_TYPES = {' | |||||
34 | b'command-data': FRAME_TYPE_COMMAND_DATA, |
|
35 | b'command-data': FRAME_TYPE_COMMAND_DATA, | |
35 | b'bytes-response': FRAME_TYPE_BYTES_RESPONSE, |
|
36 | b'bytes-response': FRAME_TYPE_BYTES_RESPONSE, | |
36 | b'error-response': FRAME_TYPE_ERROR_RESPONSE, |
|
37 | b'error-response': FRAME_TYPE_ERROR_RESPONSE, | |
|
38 | b'text-output': FRAME_TYPE_TEXT_OUTPUT, | |||
37 | } |
|
39 | } | |
38 |
|
40 | |||
39 | FLAG_COMMAND_NAME_EOS = 0x01 |
|
41 | FLAG_COMMAND_NAME_EOS = 0x01 | |
@@ -85,6 +87,7 b' FRAME_TYPE_FLAGS = {' | |||||
85 | FRAME_TYPE_COMMAND_DATA: FLAGS_COMMAND_DATA, |
|
87 | FRAME_TYPE_COMMAND_DATA: FLAGS_COMMAND_DATA, | |
86 | FRAME_TYPE_BYTES_RESPONSE: FLAGS_BYTES_RESPONSE, |
|
88 | FRAME_TYPE_BYTES_RESPONSE: FLAGS_BYTES_RESPONSE, | |
87 | FRAME_TYPE_ERROR_RESPONSE: FLAGS_ERROR_RESPONSE, |
|
89 | FRAME_TYPE_ERROR_RESPONSE: FLAGS_ERROR_RESPONSE, | |
|
90 | FRAME_TYPE_TEXT_OUTPUT: {}, | |||
88 | } |
|
91 | } | |
89 |
|
92 | |||
90 | ARGUMENT_FRAME_HEADER = struct.Struct(r'<HH') |
|
93 | ARGUMENT_FRAME_HEADER = struct.Struct(r'<HH') | |
@@ -281,6 +284,74 b' def createerrorframe(requestid, msg, pro' | |||||
281 |
|
284 | |||
282 | yield makeframe(requestid, FRAME_TYPE_ERROR_RESPONSE, flags, msg) |
|
285 | yield makeframe(requestid, FRAME_TYPE_ERROR_RESPONSE, flags, msg) | |
283 |
|
286 | |||
|
287 | def createtextoutputframe(requestid, atoms): | |||
|
288 | """Create a text output frame to render text to people. | |||
|
289 | ||||
|
290 | ``atoms`` is a 3-tuple of (formatting string, args, labels). | |||
|
291 | ||||
|
292 | The formatting string contains ``%s`` tokens to be replaced by the | |||
|
293 | corresponding indexed entry in ``args``. ``labels`` is an iterable of | |||
|
294 | formatters to be applied at rendering time. In terms of the ``ui`` | |||
|
295 | class, each atom corresponds to a ``ui.write()``. | |||
|
296 | """ | |||
|
297 | bytesleft = DEFAULT_MAX_FRAME_SIZE | |||
|
298 | atomchunks = [] | |||
|
299 | ||||
|
300 | for (formatting, args, labels) in atoms: | |||
|
301 | if len(args) > 255: | |||
|
302 | raise ValueError('cannot use more than 255 formatting arguments') | |||
|
303 | if len(labels) > 255: | |||
|
304 | raise ValueError('cannot use more than 255 labels') | |||
|
305 | ||||
|
306 | # TODO look for localstr, other types here? | |||
|
307 | ||||
|
308 | if not isinstance(formatting, bytes): | |||
|
309 | raise ValueError('must use bytes formatting strings') | |||
|
310 | for arg in args: | |||
|
311 | if not isinstance(arg, bytes): | |||
|
312 | raise ValueError('must use bytes for arguments') | |||
|
313 | for label in labels: | |||
|
314 | if not isinstance(label, bytes): | |||
|
315 | raise ValueError('must use bytes for labels') | |||
|
316 | ||||
|
317 | # Formatting string must be UTF-8. | |||
|
318 | formatting = formatting.decode(r'utf-8', r'replace').encode(r'utf-8') | |||
|
319 | ||||
|
320 | # Arguments must be UTF-8. | |||
|
321 | args = [a.decode(r'utf-8', r'replace').encode(r'utf-8') for a in args] | |||
|
322 | ||||
|
323 | # Labels must be ASCII. | |||
|
324 | labels = [l.decode(r'ascii', r'strict').encode(r'ascii') | |||
|
325 | for l in labels] | |||
|
326 | ||||
|
327 | if len(formatting) > 65535: | |||
|
328 | raise ValueError('formatting string cannot be longer than 64k') | |||
|
329 | ||||
|
330 | if any(len(a) > 65535 for a in args): | |||
|
331 | raise ValueError('argument string cannot be longer than 64k') | |||
|
332 | ||||
|
333 | if any(len(l) > 255 for l in labels): | |||
|
334 | raise ValueError('label string cannot be longer than 255 bytes') | |||
|
335 | ||||
|
336 | chunks = [ | |||
|
337 | struct.pack(r'<H', len(formatting)), | |||
|
338 | struct.pack(r'<BB', len(labels), len(args)), | |||
|
339 | struct.pack(r'<' + r'B' * len(labels), *map(len, labels)), | |||
|
340 | struct.pack(r'<' + r'H' * len(args), *map(len, args)), | |||
|
341 | ] | |||
|
342 | chunks.append(formatting) | |||
|
343 | chunks.extend(labels) | |||
|
344 | chunks.extend(args) | |||
|
345 | ||||
|
346 | atom = b''.join(chunks) | |||
|
347 | atomchunks.append(atom) | |||
|
348 | bytesleft -= len(atom) | |||
|
349 | ||||
|
350 | if bytesleft < 0: | |||
|
351 | raise ValueError('cannot encode data in a single frame') | |||
|
352 | ||||
|
353 | yield makeframe(requestid, FRAME_TYPE_TEXT_OUTPUT, 0, b''.join(atomchunks)) | |||
|
354 | ||||
284 | class serverreactor(object): |
|
355 | class serverreactor(object): | |
285 | """Holds state of a server handling frame-based protocol requests. |
|
356 | """Holds state of a server handling frame-based protocol requests. | |
286 |
|
357 |
@@ -67,6 +67,109 b' class FrameTests(unittest.TestCase):' | |||||
67 | ffs(b'1 command-data eos %s' % data.getvalue()), |
|
67 | ffs(b'1 command-data eos %s' % data.getvalue()), | |
68 | ]) |
|
68 | ]) | |
69 |
|
69 | |||
|
70 | def testtextoutputexcessiveargs(self): | |||
|
71 | """At most 255 formatting arguments are allowed.""" | |||
|
72 | with self.assertRaisesRegexp(ValueError, | |||
|
73 | 'cannot use more than 255 formatting'): | |||
|
74 | args = [b'x' for i in range(256)] | |||
|
75 | list(framing.createtextoutputframe(1, [(b'bleh', args, [])])) | |||
|
76 | ||||
|
77 | def testtextoutputexcessivelabels(self): | |||
|
78 | """At most 255 labels are allowed.""" | |||
|
79 | with self.assertRaisesRegexp(ValueError, | |||
|
80 | 'cannot use more than 255 labels'): | |||
|
81 | labels = [b'l' for i in range(256)] | |||
|
82 | list(framing.createtextoutputframe(1, [(b'bleh', [], labels)])) | |||
|
83 | ||||
|
84 | def testtextoutputformattingstringtype(self): | |||
|
85 | """Formatting string must be bytes.""" | |||
|
86 | with self.assertRaisesRegexp(ValueError, 'must use bytes formatting '): | |||
|
87 | list(framing.createtextoutputframe(1, [ | |||
|
88 | (b'foo'.decode('ascii'), [], [])])) | |||
|
89 | ||||
|
90 | def testtextoutputargumentbytes(self): | |||
|
91 | with self.assertRaisesRegexp(ValueError, 'must use bytes for argument'): | |||
|
92 | list(framing.createtextoutputframe(1, [ | |||
|
93 | (b'foo', [b'foo'.decode('ascii')], [])])) | |||
|
94 | ||||
|
95 | def testtextoutputlabelbytes(self): | |||
|
96 | with self.assertRaisesRegexp(ValueError, 'must use bytes for labels'): | |||
|
97 | list(framing.createtextoutputframe(1, [ | |||
|
98 | (b'foo', [], [b'foo'.decode('ascii')])])) | |||
|
99 | ||||
|
100 | def testtextoutputtoolongformatstring(self): | |||
|
101 | with self.assertRaisesRegexp(ValueError, | |||
|
102 | 'formatting string cannot be longer than'): | |||
|
103 | list(framing.createtextoutputframe(1, [ | |||
|
104 | (b'x' * 65536, [], [])])) | |||
|
105 | ||||
|
106 | def testtextoutputtoolongargumentstring(self): | |||
|
107 | with self.assertRaisesRegexp(ValueError, | |||
|
108 | 'argument string cannot be longer than'): | |||
|
109 | list(framing.createtextoutputframe(1, [ | |||
|
110 | (b'bleh', [b'x' * 65536], [])])) | |||
|
111 | ||||
|
112 | def testtextoutputtoolonglabelstring(self): | |||
|
113 | with self.assertRaisesRegexp(ValueError, | |||
|
114 | 'label string cannot be longer than'): | |||
|
115 | list(framing.createtextoutputframe(1, [ | |||
|
116 | (b'bleh', [], [b'x' * 65536])])) | |||
|
117 | ||||
|
118 | def testtextoutput1simpleatom(self): | |||
|
119 | val = list(framing.createtextoutputframe(1, [ | |||
|
120 | (b'foo', [], [])])) | |||
|
121 | ||||
|
122 | self.assertEqual(val, [ | |||
|
123 | ffs(br'1 text-output 0 \x03\x00\x00\x00foo'), | |||
|
124 | ]) | |||
|
125 | ||||
|
126 | def testtextoutput2simpleatoms(self): | |||
|
127 | val = list(framing.createtextoutputframe(1, [ | |||
|
128 | (b'foo', [], []), | |||
|
129 | (b'bar', [], []), | |||
|
130 | ])) | |||
|
131 | ||||
|
132 | self.assertEqual(val, [ | |||
|
133 | ffs(br'1 text-output 0 \x03\x00\x00\x00foo\x03\x00\x00\x00bar'), | |||
|
134 | ]) | |||
|
135 | ||||
|
136 | def testtextoutput1arg(self): | |||
|
137 | val = list(framing.createtextoutputframe(1, [ | |||
|
138 | (b'foo %s', [b'val1'], []), | |||
|
139 | ])) | |||
|
140 | ||||
|
141 | self.assertEqual(val, [ | |||
|
142 | ffs(br'1 text-output 0 \x06\x00\x00\x01\x04\x00foo %sval1'), | |||
|
143 | ]) | |||
|
144 | ||||
|
145 | def testtextoutput2arg(self): | |||
|
146 | val = list(framing.createtextoutputframe(1, [ | |||
|
147 | (b'foo %s %s', [b'val', b'value'], []), | |||
|
148 | ])) | |||
|
149 | ||||
|
150 | self.assertEqual(val, [ | |||
|
151 | ffs(br'1 text-output 0 \x09\x00\x00\x02\x03\x00\x05\x00' | |||
|
152 | br'foo %s %svalvalue'), | |||
|
153 | ]) | |||
|
154 | ||||
|
155 | def testtextoutput1label(self): | |||
|
156 | val = list(framing.createtextoutputframe(1, [ | |||
|
157 | (b'foo', [], [b'label']), | |||
|
158 | ])) | |||
|
159 | ||||
|
160 | self.assertEqual(val, [ | |||
|
161 | ffs(br'1 text-output 0 \x03\x00\x01\x00\x05foolabel'), | |||
|
162 | ]) | |||
|
163 | ||||
|
164 | def testargandlabel(self): | |||
|
165 | val = list(framing.createtextoutputframe(1, [ | |||
|
166 | (b'foo %s', [b'arg'], [b'label']), | |||
|
167 | ])) | |||
|
168 | ||||
|
169 | self.assertEqual(val, [ | |||
|
170 | ffs(br'1 text-output 0 \x06\x00\x01\x01\x05\x03\x00foo %slabelarg'), | |||
|
171 | ]) | |||
|
172 | ||||
70 | class ServerReactorTests(unittest.TestCase): |
|
173 | class ServerReactorTests(unittest.TestCase): | |
71 | def _sendsingleframe(self, reactor, s): |
|
174 | def _sendsingleframe(self, reactor, s): | |
72 | results = list(sendframes(reactor, [ffs(s)])) |
|
175 | results = list(sendframes(reactor, [ffs(s)])) |
General Comments 0
You need to be logged in to leave comments.
Login now