From 9825f254a8515be0048ebcdeda7ae0186038a4a1 Mon Sep 17 00:00:00 2001 From: Jonathan Piacenti Date: Sat, 12 Aug 2017 19:15:35 -0500 Subject: [PATCH] Force ANSIString into bytestring before sessions send it over the wire. --- evennia/server/sessionhandler.py | 7 +- evennia/utils/ansi.py | 175 +++++++++++++++++++++++++------ 2 files changed, 150 insertions(+), 32 deletions(-) diff --git a/evennia/server/sessionhandler.py b/evennia/server/sessionhandler.py index 6af7e6aed1..8acaab15c1 100644 --- a/evennia/server/sessionhandler.py +++ b/evennia/server/sessionhandler.py @@ -201,7 +201,12 @@ class SessionHandler(dict): if _INLINEFUNC_ENABLED and not raw and isinstance(self, ServerSessionHandler): # only parse inlinefuncs on the outgoing path (sessionhandler->) data = parse_inlinefunc(data, strip=strip_inlinefunc, session=session) - return data + # At this point the object is certainly the right encoding, but may still be a unicode object-- + # to_str does not actually force objects to become bytestrings. + # If the unicode object is a subclass of unicode, such as ANSIString, this can cause a problem, + # as special behavior for that class will still be in play. Since we're now transferring raw data, + # we must now force this to be a proper bytestring. + return str(data) elif hasattr(data, "id") and hasattr(data, "db_date_created") \ and hasattr(data, '__dbclass__'): # convert database-object to their string representation. diff --git a/evennia/utils/ansi.py b/evennia/utils/ansi.py index dead3e00bf..c44bf5caab 100644 --- a/evennia/utils/ansi.py +++ b/evennia/utils/ansi.py @@ -83,6 +83,7 @@ class ANSIParser(object): We also allow to escape colour codes by prepending with a \ for xterm256, an extra | for Merc-style codes + """ def sub_ansi(self, ansimatch): @@ -461,7 +462,7 @@ class ANSIParser(object): (r'\{\[=[a-z]', ""), (r'\|=[a-z]', ""), (r'\|\[=[a-z]', ""), - ] + ] mxp_re = r'\|lc(.*?)\|lt(.*?)\|le' @@ -535,6 +536,7 @@ def strip_raw_ansi(string, parser=ANSI_PARSER): Returns: string (str): the stripped string. + """ return parser.strip_raw_codes(string) @@ -551,13 +553,6 @@ def raw(string): return string.replace('{', '{{').replace('|', '||') -def group(lst, n): - for i in range(0, len(lst), n): - val = lst[i:i+n] - if len(val) == n: - yield tuple(val) - - def _spacing_preflight(func): """ This wrapper function is used to do some preflight checks on @@ -571,10 +566,10 @@ def _spacing_preflight(func): raise TypeError("must be char, not %s" % type(fillchar)) if not isinstance(width, int): raise TypeError("integer argument expected, got %s" % type(width)) - difference = width - len(self) - if difference <= 0: + _difference = width - len(self) + if _difference <= 0: return self - return func(self, width, fillchar, difference) + return func(self, width, fillchar, _difference) return wrapped @@ -661,20 +656,30 @@ class ANSIMeta(type): class ANSIString(with_metaclass(ANSIMeta, unicode)): """ - String-like object that is aware of ANSI codes. + Unicode-like object that is aware of ANSI codes. - This isn't especially efficient, as it doesn't really have an + This class can be used nearly identically to unicode, in that it will + report string length, handle slices, etc, much like a unicode or + string object would. The methods should be used identically as unicode + methods are. + + There is at least one exception to this (and there may be more, though + they have not come up yet). When using ''.join() or u''.join() on an + ANSIString, color information will get lost. You must use + ANSIString('').join() to preserve color information. + + This implementation isn't perfectly clean, as it doesn't really have an understanding of what the codes mean in order to eliminate - redundant characters. This could be made as an enhancement to ANSI_PARSER. + redundant characters-- though cleaning up the strings might end up being + inefficient and slow without some C code when dealing with larger values. + Such enhancements could be made as an enhancement to ANSI_PARSER + if needed, however. If one is going to use ANSIString, one should generally avoid converting away from it until one is about to send information on the wire. This is because escape sequences in the string may otherwise already be decoded, and taken literally the second time around. - Please refer to the Metaclass, ANSIMeta, which is used to apply wrappers - for several of the methods that need not be defined directly here. - """ def __new__(cls, *args, **kwargs): @@ -922,6 +927,9 @@ class ANSIString(with_metaclass(ANSIMeta, unicode)): """ Return a unicode object without the ANSI escapes. + Returns: + clean_string (unicode): A unicode object with no ANSI escapes. + """ return self._clean_string @@ -929,20 +937,31 @@ class ANSIString(with_metaclass(ANSIMeta, unicode)): """ Return a unicode object with the ANSI escapes. + Returns: + raw (unicode): A unicode object with the raw ANSI escape sequences. + """ return self._raw_string def partition(self, sep, reverse=False): """ - Similar to split, but always creates a tuple with three items: - - 1. The part before the separator - 2. The separator itself. - 3. The part after. + Splits once into three sections (with the separator being the middle section) We use the same techniques we used in split() to make sure each are colored. + Args: + sep (str): The separator to split the string on. + reverse (boolean): Whether to split the string on the last + occurrence of the separator rather than the first. + Returns: + result (tuple): + prefix (ANSIString): The part of the string before the + separator + sep (ANSIString): The separator itself + postfix (ANSIString): The part of the string after the + separator. + """ if hasattr(sep, '_clean_string'): sep = sep.clean() @@ -1032,12 +1051,26 @@ class ANSIString(with_metaclass(ANSIMeta, unicode)): def split(self, by=None, maxsplit=-1): """ + Splits a string based on a separator. + Stolen from PyPy's pure Python string implementation, tweaked for ANSIString. PyPy is distributed under the MIT licence. http://opensource.org/licenses/MIT + Args: + by (str): A string to search for which will be used to split + the string. For instance, ',' for 'Hello,world' would + result in ['Hello', 'world'] + maxsplit (int): The maximum number of times to split the string. + For example, a maxsplit of 2 with a by of ',' on the string + 'Hello,world,test,string' would result in + ['Hello', 'world', 'test,string'] + Returns: + result (list of ANSIStrings): A list of ANSIStrings derived from + this string. + """ drop_spaces = by is None if drop_spaces: @@ -1065,12 +1098,27 @@ class ANSIString(with_metaclass(ANSIMeta, unicode)): def rsplit(self, by=None, maxsplit=-1): """ + Like split, but starts from the end of the string rather than the + beginning. + Stolen from PyPy's pure Python string implementation, tweaked for ANSIString. PyPy is distributed under the MIT licence. http://opensource.org/licenses/MIT + Args: + by (str): A string to search for which will be used to split + the string. For instance, ',' for 'Hello,world' would + result in ['Hello', 'world'] + maxsplit (int): The maximum number of times to split the string. + For example, a maxsplit of 2 with a by of ',' on the string + 'Hello,world,test,string' would result in + ['Hello,world', 'test', 'string'] + Returns: + result (list of ANSIStrings): A list of ANSIStrings derived from + this string. + """ res = [] end = len(self) @@ -1099,6 +1147,15 @@ class ANSIString(with_metaclass(ANSIMeta, unicode)): def strip(self, chars=None): """ Strip from both ends, taking ANSI markers into account. + + Args: + chars (str, optional): A string containing individual characters + to strip off of both ends of the string. By default, any blank + spaces are trimmed. + Returns: + result (ANSIString): A new ANSIString with the ends trimmed of the + relevant characters. + """ clean = self._clean_string raw = self._raw_string @@ -1136,6 +1193,15 @@ class ANSIString(with_metaclass(ANSIMeta, unicode)): def lstrip(self, chars=None): """ Strip from the left, taking ANSI markers into account. + + Args: + chars (str, optional): A string containing individual characters + to strip off of the left end of the string. By default, any + blank spaces are trimmed. + Returns: + result (ANSIString): A new ANSIString with the left end trimmed of + the relevant characters. + """ clean = self._clean_string raw = self._raw_string @@ -1160,6 +1226,15 @@ class ANSIString(with_metaclass(ANSIMeta, unicode)): def rstrip(self, chars=None): """ Strip from the right, taking ANSI markers into account. + + Args: + chars (str, optional): A string containing individual characters + to strip off of the right end of the string. By default, any + blank spaces are trimmed. + Returns: + result (ANSIString): A new ANSIString with the right end trimmed of + the relevant characters. + """ clean = self._clean_string raw = self._raw_string @@ -1180,7 +1255,22 @@ class ANSIString(with_metaclass(ANSIMeta, unicode)): def join(self, iterable): """ - Joins together strings in an iterable. + Joins together strings in an iterable, using this string between each + one. + + NOTE: This should always be used for joining strings when ANSIStrings + are involved. Otherwise color information will be discarded by + python, due to details in the C implementation of unicode strings. + + Args: + iterable (list of strings): A list of strings to join together + Returns: + result (ANSIString): A single string with all of the iterable's + contents concatenated, with this string between each. For + example: + ANSIString(', ').join(['up', 'right', 'left', 'down']) + ...Would return: + ANSIString('up, right, left, down') """ result = ANSIString('') @@ -1222,30 +1312,53 @@ class ANSIString(with_metaclass(ANSIMeta, unicode)): raw_string, clean_string=line, char_indexes=char_indexes, code_indexes=code_indexes) + # The following methods should not be called with the '_difference' argument explicitly. This is + # data provided by the wrapper _spacing_preflight. @_spacing_preflight - def center(self, width, fillchar, difference): + def center(self, width, fillchar, _difference): """ Center some text with some spaces padding both sides. + Args: + width (int): The target width of the output string. + fillchar (str): A single character string to pad the output string + with. + Returns: + result (ANSIString): A string padded on both ends with fillchar. + """ - remainder = difference % 2 - difference /= 2 - spacing = self._filler(fillchar, difference) + remainder = _difference % 2 + _difference /= 2 + spacing = self._filler(fillchar, _difference) result = spacing + self + spacing + self._filler(fillchar, remainder) return result @_spacing_preflight - def ljust(self, width, fillchar, difference): + def ljust(self, width, fillchar, _difference): """ Left justify some text. + Args: + width (int): The target width of the output string. + fillchar (str): A single character string to pad the output string + with. + Returns: + result (ANSIString): A string padded on the right with fillchar. + """ - return self + self._filler(fillchar, difference) + return self + self._filler(fillchar, _difference) @_spacing_preflight - def rjust(self, width, fillchar, difference): + def rjust(self, width, fillchar, _difference): """ Right justify some text. + Args: + width (int): The target width of the output string. + fillchar (str): A single character string to pad the output string + with. + Returns: + result (ANSIString): A string padded on the left with fillchar. + """ - return self._filler(fillchar, difference) + self + return self._filler(fillchar, _difference) + self