D2872: wireproto: define human output side channel frame
indygreg (Gregory Szorc)
phabricator at mercurial-scm.org
Mon Mar 19 20:01:15 EDT 2018
indygreg updated this revision to Diff 7148.
REPOSITORY
rHG Mercurial
CHANGES SINCE LAST UPDATE
https://phab.mercurial-scm.org/D2872?vs=7057&id=7148
REVISION DETAIL
https://phab.mercurial-scm.org/D2872
AFFECTED FILES
mercurial/help/internals/wireprotocol.txt
mercurial/wireprotoframing.py
tests/test-wireproto-serverreactor.py
CHANGE DETAILS
diff --git a/tests/test-wireproto-serverreactor.py b/tests/test-wireproto-serverreactor.py
--- a/tests/test-wireproto-serverreactor.py
+++ b/tests/test-wireproto-serverreactor.py
@@ -67,6 +67,109 @@
ffs(b'1 command-data eos %s' % data.getvalue()),
])
+ def testtextoutputexcessiveargs(self):
+ """At most 255 formatting arguments are allowed."""
+ with self.assertRaisesRegexp(ValueError,
+ 'cannot use more than 255 formatting'):
+ args = [b'x' for i in range(256)]
+ list(framing.createtextoutputframe(1, [(b'bleh', args, [])]))
+
+ def testtextoutputexcessivelabels(self):
+ """At most 255 labels are allowed."""
+ with self.assertRaisesRegexp(ValueError,
+ 'cannot use more than 255 labels'):
+ labels = [b'l' for i in range(256)]
+ list(framing.createtextoutputframe(1, [(b'bleh', [], labels)]))
+
+ def testtextoutputformattingstringtype(self):
+ """Formatting string must be bytes."""
+ with self.assertRaisesRegexp(ValueError, 'must use bytes formatting '):
+ list(framing.createtextoutputframe(1, [
+ (b'foo'.decode('ascii'), [], [])]))
+
+ def testtextoutputargumentbytes(self):
+ with self.assertRaisesRegexp(ValueError, 'must use bytes for argument'):
+ list(framing.createtextoutputframe(1, [
+ (b'foo', [b'foo'.decode('ascii')], [])]))
+
+ def testtextoutputlabelbytes(self):
+ with self.assertRaisesRegexp(ValueError, 'must use bytes for labels'):
+ list(framing.createtextoutputframe(1, [
+ (b'foo', [], [b'foo'.decode('ascii')])]))
+
+ def testtextoutputtoolongformatstring(self):
+ with self.assertRaisesRegexp(ValueError,
+ 'formatting string cannot be longer than'):
+ list(framing.createtextoutputframe(1, [
+ (b'x' * 65536, [], [])]))
+
+ def testtextoutputtoolongargumentstring(self):
+ with self.assertRaisesRegexp(ValueError,
+ 'argument string cannot be longer than'):
+ list(framing.createtextoutputframe(1, [
+ (b'bleh', [b'x' * 65536], [])]))
+
+ def testtextoutputtoolonglabelstring(self):
+ with self.assertRaisesRegexp(ValueError,
+ 'label string cannot be longer than'):
+ list(framing.createtextoutputframe(1, [
+ (b'bleh', [], [b'x' * 65536])]))
+
+ def testtextoutput1simpleatom(self):
+ val = list(framing.createtextoutputframe(1, [
+ (b'foo', [], [])]))
+
+ self.assertEqual(val, [
+ ffs(br'1 text-output 0 \x03\x00\x00\x00foo'),
+ ])
+
+ def testtextoutput2simpleatoms(self):
+ val = list(framing.createtextoutputframe(1, [
+ (b'foo', [], []),
+ (b'bar', [], []),
+ ]))
+
+ self.assertEqual(val, [
+ ffs(br'1 text-output 0 \x03\x00\x00\x00foo\x03\x00\x00\x00bar'),
+ ])
+
+ def testtextoutput1arg(self):
+ val = list(framing.createtextoutputframe(1, [
+ (b'foo %s', [b'val1'], []),
+ ]))
+
+ self.assertEqual(val, [
+ ffs(br'1 text-output 0 \x06\x00\x00\x01\x04\x00foo %sval1'),
+ ])
+
+ def testtextoutput2arg(self):
+ val = list(framing.createtextoutputframe(1, [
+ (b'foo %s %s', [b'val', b'value'], []),
+ ]))
+
+ self.assertEqual(val, [
+ ffs(br'1 text-output 0 \x09\x00\x00\x02\x03\x00\x05\x00'
+ br'foo %s %svalvalue'),
+ ])
+
+ def testtextoutput1label(self):
+ val = list(framing.createtextoutputframe(1, [
+ (b'foo', [], [b'label']),
+ ]))
+
+ self.assertEqual(val, [
+ ffs(br'1 text-output 0 \x03\x00\x01\x00\x05foolabel'),
+ ])
+
+ def testargandlabel(self):
+ val = list(framing.createtextoutputframe(1, [
+ (b'foo %s', [b'arg'], [b'label']),
+ ]))
+
+ self.assertEqual(val, [
+ ffs(br'1 text-output 0 \x06\x00\x01\x01\x05\x03\x00foo %slabelarg'),
+ ])
+
class ServerReactorTests(unittest.TestCase):
def _sendsingleframe(self, reactor, s):
results = list(sendframes(reactor, [ffs(s)]))
diff --git a/mercurial/wireprotoframing.py b/mercurial/wireprotoframing.py
--- a/mercurial/wireprotoframing.py
+++ b/mercurial/wireprotoframing.py
@@ -27,13 +27,15 @@
FRAME_TYPE_COMMAND_DATA = 0x03
FRAME_TYPE_BYTES_RESPONSE = 0x04
FRAME_TYPE_ERROR_RESPONSE = 0x05
+FRAME_TYPE_TEXT_OUTPUT = 0x06
FRAME_TYPES = {
b'command-name': FRAME_TYPE_COMMAND_NAME,
b'command-argument': FRAME_TYPE_COMMAND_ARGUMENT,
b'command-data': FRAME_TYPE_COMMAND_DATA,
b'bytes-response': FRAME_TYPE_BYTES_RESPONSE,
b'error-response': FRAME_TYPE_ERROR_RESPONSE,
+ b'text-output': FRAME_TYPE_TEXT_OUTPUT,
}
FLAG_COMMAND_NAME_EOS = 0x01
@@ -85,6 +87,7 @@
FRAME_TYPE_COMMAND_DATA: FLAGS_COMMAND_DATA,
FRAME_TYPE_BYTES_RESPONSE: FLAGS_BYTES_RESPONSE,
FRAME_TYPE_ERROR_RESPONSE: FLAGS_ERROR_RESPONSE,
+ FRAME_TYPE_TEXT_OUTPUT: {},
}
ARGUMENT_FRAME_HEADER = struct.Struct(r'<HH')
@@ -281,6 +284,74 @@
yield makeframe(requestid, FRAME_TYPE_ERROR_RESPONSE, flags, msg)
+def createtextoutputframe(requestid, atoms):
+ """Create a text output frame to render text to people.
+
+ ``atoms`` is a 3-tuple of (formatting string, args, labels).
+
+ The formatting string contains ``%s`` tokens to be replaced by the
+ corresponding indexed entry in ``args``. ``labels`` is an iterable of
+ formatters to be applied at rendering time. In terms of the ``ui``
+ class, each atom corresponds to a ``ui.write()``.
+ """
+ bytesleft = DEFAULT_MAX_FRAME_SIZE
+ atomchunks = []
+
+ for (formatting, args, labels) in atoms:
+ if len(args) > 255:
+ raise ValueError('cannot use more than 255 formatting arguments')
+ if len(labels) > 255:
+ raise ValueError('cannot use more than 255 labels')
+
+ # TODO look for localstr, other types here?
+
+ if not isinstance(formatting, bytes):
+ raise ValueError('must use bytes formatting strings')
+ for arg in args:
+ if not isinstance(arg, bytes):
+ raise ValueError('must use bytes for arguments')
+ for label in labels:
+ if not isinstance(label, bytes):
+ raise ValueError('must use bytes for labels')
+
+ # Formatting string must be UTF-8.
+ formatting = formatting.decode(r'utf-8', r'replace').encode(r'utf-8')
+
+ # Arguments must be UTF-8.
+ args = [a.decode(r'utf-8', r'replace').encode(r'utf-8') for a in args]
+
+ # Labels must be ASCII.
+ labels = [l.decode(r'ascii', r'strict').encode(r'ascii')
+ for l in labels]
+
+ if len(formatting) > 65535:
+ raise ValueError('formatting string cannot be longer than 64k')
+
+ if any(len(a) > 65535 for a in args):
+ raise ValueError('argument string cannot be longer than 64k')
+
+ if any(len(l) > 255 for l in labels):
+ raise ValueError('label string cannot be longer than 255 bytes')
+
+ chunks = [
+ struct.pack(r'<H', len(formatting)),
+ struct.pack(r'<BB', len(labels), len(args)),
+ struct.pack(r'<' + r'B' * len(labels), *map(len, labels)),
+ struct.pack(r'<' + r'H' * len(args), *map(len, args)),
+ ]
+ chunks.append(formatting)
+ chunks.extend(labels)
+ chunks.extend(args)
+
+ atom = b''.join(chunks)
+ atomchunks.append(atom)
+ bytesleft -= len(atom)
+
+ if bytesleft < 0:
+ raise ValueError('cannot encode data in a single frame')
+
+ yield makeframe(requestid, FRAME_TYPE_TEXT_OUTPUT, 0, b''.join(atomchunks))
+
class serverreactor(object):
"""Holds state of a server handling frame-based protocol requests.
diff --git a/mercurial/help/internals/wireprotocol.txt b/mercurial/help/internals/wireprotocol.txt
--- a/mercurial/help/internals/wireprotocol.txt
+++ b/mercurial/help/internals/wireprotocol.txt
@@ -660,6 +660,64 @@
0x02
The error occurred at the application level. e.g. invalid command.
+Human Output Side-Channel (``0x06``)
+------------------------------------
+
+This frame contains a message that is intended to be displayed to
+people. Whereas most frames communicate machine readable data, this
+frame communicates textual data that is intended to be shown to
+humans.
+
+The frame consists of a series of *formatting requests*. Each formatting
+request consists of a formatting string, arguments for that formatting
+string, and labels to apply to that formatting string.
+
+A formatting string is a printf()-like string that allows variable
+substitution within the string. Labels allow the rendered text to be
+*decorated*. Assuming use of the canonical Mercurial code base, a
+formatting string can be the input to the ``i18n._`` function. This
+allows messages emitted from the server to be localized. So even if
+the server has different i18n settings, people could see messages in
+their *native* settings. Similarly, the use of labels allows
+decorations like coloring and underlining to be applied using the
+client's configured rendering settings.
+
+Formatting strings are similar to ``printf()`` strings or how
+Python's ``%`` operator works. The only supported formatting sequences
+are ``%s`` and ``%%``. ``%s`` will be replaced by whatever the string
+at that position resolves to. ``%%`` will be replaced by ``%``. All
+other 2-byte sequences beginning with ``%`` represent a literal
+``%`` followed by that character. However, future versions of the
+wire protocol reserve the right to allow clients to opt in to receiving
+formatting strings with additional formatters, hence why ``%%`` is
+required to represent the literal ``%``.
+
+The raw frame consists of a series of data structures representing
+textual atoms to print. Each atom begins with a struct defining the
+size of the data that follows:
+
+* A 16-bit little endian unsigned integer denoting the length of the
+ formatting string.
+* An 8-bit unsigned integer denoting the number of label strings
+ that follow.
+* An 8-bit unsigned integer denoting the number of formatting string
+ arguments strings that follow.
+* An array of 8-bit unsigned integers denoting the lengths of
+ *labels* data.
+* An array of 16-bit unsigned integers denoting the lengths of
+ formatting strings.
+* The formatting string, encoded as UTF-8.
+* 0 or more ASCII strings defining labels to apply to this atom.
+* 0 or more UTF-8 strings that will be used as arguments to the
+ formatting string.
+
+All data to be printed MUST be encoded into a single frame: this frame
+does not support spanning data across multiple frames.
+
+All textual data encoded in these frames is assumed to be line delimited.
+The last atom in the frame SHOULD end with a newline (``\n``). If it
+doesn't, clients MAY add a newline to facilitate immediate printing.
+
Issuing Commands
----------------
To: indygreg, #hg-reviewers
Cc: mercurial-devel
More information about the Mercurial-devel
mailing list