Patchwork D2872: wireproto: define human output side channel frame

login
register
mail settings
Submitter phabricator
Date March 20, 2018, 12:01 a.m.
Message ID <9f38b5cb3d5a06403f29be91bc80ec87@localhost.localdomain>
Download mbox | patch
Permalink /patch/29658/
State Not Applicable
Headers show

Comments

phabricator - March 20, 2018, 12:01 a.m.
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




To: indygreg, #hg-reviewers
Cc: mercurial-devel

Patch

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
 ----------------