From 7b951f1a57ba311dc6b03ab16d6a6727a3218c13 Mon Sep 17 00:00:00 2001
From: MrQubo <jakub.jakub.nowak@gmail.com>
Date: Sat, 28 Sep 2024 23:16:27 +0200
Subject: [PATCH 1/3] Replace 'keepends' with 'drop'

Add 'drop' argument in place of 'keepends' for tubes. 'keepends' still
works but raises depreciation warning.
---
 pwnlib/context/__init__.py |   8 +-
 pwnlib/tubes/tube.py       | 146 +++++++++++++++++++++++++------------
 2 files changed, 104 insertions(+), 50 deletions(-)

diff --git a/pwnlib/context/__init__.py b/pwnlib/context/__init__.py
index 670fb138a..a95ec4310 100644
--- a/pwnlib/context/__init__.py
+++ b/pwnlib/context/__init__.py
@@ -594,9 +594,9 @@ def quiet(self, function=None):
             ...         log.debug("DEBUG")
             ...         log.info("INFO")
             ...         log.warn("WARN")
-            [DEBUG] DEBUG
-            [*] INFO
-            [!] WARN
+            [...] DEBUG
+            [...] INFO
+            [...] WARN
         """
         level = 'error'
         if context.log_level <= logging.DEBUG:
@@ -664,7 +664,7 @@ def verbose(self):
             information is printed.
 
             >>> with context.verbose: func()
-            [DEBUG] Hello
+            [...] Hello
 
         """
         return self.local(log_level='debug')
diff --git a/pwnlib/tubes/tube.py b/pwnlib/tubes/tube.py
index 84798314f..8e8405ae0 100644
--- a/pwnlib/tubes/tube.py
+++ b/pwnlib/tubes/tube.py
@@ -46,6 +46,44 @@ def __init__(self, timeout = default, level = None, *a, **kw):
         self._newline = None
         atexit.register(self.close)
 
+    def _normalize_keepends_drop(self, keepends, drop, drop_default):
+        '''
+        >>> t = tube()
+        >>> t._normalize_keepends_drop(None, None, True)
+        True
+        >>> t._normalize_keepends_drop(None, None, False)
+        False
+        >>> t._normalize_keepends_drop(None, True, True)
+        True
+        >>> t._normalize_keepends_drop(None, True, False)
+        True
+        >>> t._normalize_keepends_drop(True, None, True)
+        False
+        >>> t._normalize_keepends_drop(True, None, False)
+        False
+        >>> t._normalize_keepends_drop(None, False, True)
+        False
+        >>> t._normalize_keepends_drop(None, False, False)
+        False
+        >>> t._normalize_keepends_drop(False, None, True)
+        True
+        >>> t._normalize_keepends_drop(False, None, False)
+        True
+        >>> t._normalize_keepends_drop(False, True, False)
+        Traceback (most recent call last):
+            ...
+        pwnlib.exception.PwnlibException: 'drop' and 'keepends' arguments cannot be used together.
+        '''
+        if keepends is not None:
+            self.warn_once("'keepends' argument is deprecated. Use 'drop' instead.")
+        if drop is None and keepends is None:
+            return drop_default
+        elif drop is not None:
+            if keepends is not None:
+                self.error("'drop' and 'keepends' arguments cannot be used together.")
+            return drop
+        return not keepends
+
     @property
     def newline(self):
         r'''Character sent with methods like sendline() or used for recvline().
@@ -100,7 +138,7 @@ def recv(self, numb = None, timeout = default):
             >>> t.recv() == b'Woohoo'
             True
             >>> with context.local(log_level='debug'):
-            ...    _ = t.recv() # doctest: +ELLIPSIS
+            ...    _ = t.recv()
             [...] Received 0xc bytes:
                 b'Hello, world'
         """
@@ -265,7 +303,7 @@ def recvn(self, numb, timeout = default):
             >>> t.recv_raw = lambda *a: time.sleep(0.01) or b'a'
             >>> t.recvn(10, timeout=0.05)
             b''
-            >>> t.recvn(10, timeout=0.06) # doctest: +ELLIPSIS
+            >>> t.recvn(10, timeout=0.06)
             b'aaaaaa...'
         """
         # Keep track of how much data has been received
@@ -370,8 +408,8 @@ def recvuntil(self, delims, drop=False, timeout=default):
 
         return b''
 
-    def recvlines(self, numlines=2**20, keepends=False, timeout=default):
-        r"""recvlines(numlines, keepends=False, timeout=default) -> list of bytes objects
+    def recvlines(self, numlines=2**20, keepends=None, drop=None, timeout=default):
+        r"""recvlines(numlines, drop=True, timeout=default) -> list of bytes objects
 
         Receive up to ``numlines`` lines.
 
@@ -383,7 +421,7 @@ def recvlines(self, numlines=2**20, keepends=False, timeout=default):
 
         Arguments:
             numlines(int): Maximum number of lines to receive
-            keepends(bool): Keep newlines at the end of each line (:const:`False`).
+            drop(bool): Drop newlines at the end of each line (:const:`True`).
             timeout(int): Maximum timeout
 
         Raises:
@@ -404,15 +442,20 @@ def recvlines(self, numlines=2**20, keepends=False, timeout=default):
             [b'Foo', b'Bar', b'Baz']
             >>> t.recvlines(3, True)
             [b'Foo\n', b'Bar\n', b'Baz\n']
+            >>> t.recvlines(3, drop=False)
+            [b'Foo\n', b'Bar\n', b'Baz\n']
         """
+        drop = self._normalize_keepends_drop(keepends, drop, True)
+        del keepends
+
         lines = []
         with self.countdown(timeout):
             for _ in range(numlines):
                 try:
-                    # We must set 'keepends' to True here so that we can
+                    # We must set 'drop' to False here so that we can
                     # restore the original, unmodified data to the buffer
                     # in the event of a timeout.
-                    res = self.recvline(keepends=True, timeout=timeout)
+                    res = self.recvline(drop=False, timeout=timeout)
                 except Exception:
                     self.unrecv(b''.join(lines))
                     raise
@@ -422,13 +465,13 @@ def recvlines(self, numlines=2**20, keepends=False, timeout=default):
                 else:
                     break
 
-        if not keepends:
+        if drop:
             lines = [line.rstrip(self.newline) for line in lines]
 
         return lines
 
-    def recvlinesS(self, numlines=2**20, keepends=False, timeout=default):
-        r"""recvlinesS(numlines, keepends=False, timeout=default) -> str list
+    def recvlinesS(self, numlines=2**20, keepends=None, drop=None, timeout=default):
+        r"""recvlinesS(numlines, drop=True, timeout=default) -> str list
 
         This function is identical to :meth:`recvlines`, but decodes
         the received bytes into string using :func:`context.encoding`.
@@ -444,10 +487,10 @@ def recvlinesS(self, numlines=2**20, keepends=False, timeout=default):
             >>> t.recvlinesS(3)
             ['Foo', 'Bar', 'Baz']
         """
-        return [packing._decode(x) for x in self.recvlines(numlines, keepends, timeout)]
+        return [packing._decode(x) for x in self.recvlines(numlines, keepends=keepends, drop=drop, timeout=timeout)]
 
-    def recvlinesb(self, numlines=2**20, keepends=False, timeout=default):
-        r"""recvlinesb(numlines, keepends=False, timeout=default) -> bytearray list
+    def recvlinesb(self, numlines=2**20, keepends=None, drop=None, timeout=default):
+        r"""recvlinesb(numlines, drop=True, timeout=default) -> bytearray list
 
         This function is identical to :meth:`recvlines`, but returns a bytearray.
 
@@ -461,10 +504,10 @@ def recvlinesb(self, numlines=2**20, keepends=False, timeout=default):
             >>> t.recvlinesb(3)
             [bytearray(b'Foo'), bytearray(b'Bar'), bytearray(b'Baz')]
         """
-        return [bytearray(x) for x in self.recvlines(numlines, keepends, timeout)]
+        return [bytearray(x) for x in self.recvlines(numlines, keepends=keepends, drop=drop, timeout=timeout)]
 
-    def recvline(self, keepends=True, timeout=default):
-        r"""recvline(keepends=True, timeout=default) -> bytes
+    def recvline(self, keepends=None, drop=None, timeout=default):
+        r"""recvline(drop=False, timeout=default) -> bytes
 
         Receive a single line from the tube.
 
@@ -480,7 +523,7 @@ def recvline(self, keepends=True, timeout=default):
         all data is buffered and an empty byte string (``b''``) is returned.
 
         Arguments:
-            keepends(bool): Keep the line ending (:const:`True`).
+            drop(bool): Drop the line ending (:const:`False`).
             timeout(int): Timeout
 
         Raises:
@@ -503,10 +546,10 @@ def recvline(self, keepends=True, timeout=default):
             b'Foo\n'
             >>> t.recvline()
             b'Bar\r\n'
-            >>> t.recvline(keepends = False)
+            >>> t.recvline(False)
             b'Baz'
             >>> t.newline = b'\r\n'
-            >>> t.recvline(keepends = False)
+            >>> t.recvline(drop=True)
             b'Foo\nBar'
             >>> t = tube()
             >>> def _recv_eof(n):
@@ -520,13 +563,16 @@ def recvline(self, keepends=True, timeout=default):
             b'real line\n'
             >>> t.recvline()
             b'trailing data'
-            >>> t.recvline() # doctest: +ELLIPSIS
+            >>> t.recvline()
             Traceback (most recent call last):
-            ...
+                ...
             EOFError
         """
+        drop = self._normalize_keepends_drop(keepends, drop, False)
+        del keepends
+
         try:
-            return self.recvuntil(self.newline, drop = not keepends, timeout = timeout)
+            return self.recvuntil(self.newline, drop=drop, timeout=timeout)
         except EOFError:
             if not context.throw_eof_on_incomplete_line and self.buffer.size > 0:
                 if context.throw_eof_on_incomplete_line is None:
@@ -534,8 +580,8 @@ def recvline(self, keepends=True, timeout=default):
                 return self.buffer.get()
             raise
 
-    def recvline_pred(self, pred, keepends=False, timeout=default):
-        r"""recvline_pred(pred, keepends=False) -> bytes
+    def recvline_pred(self, pred, keepends=None, drop=None, timeout=default):
+        r"""recvline_pred(pred, drop=True, timeout=default) -> bytes
 
         Receive data until ``pred(line)`` returns a truthy value.
         Drop all other data.
@@ -546,6 +592,7 @@ def recvline_pred(self, pred, keepends=False, timeout=default):
         Arguments:
             pred(callable): Function to call.  Returns the line for which
                 this function returns :const:`True`.
+            drop(bool): Drop the line ending (:const:`True`).
 
         Examples:
 
@@ -553,18 +600,22 @@ def recvline_pred(self, pred, keepends=False, timeout=default):
             >>> t.recv_raw = lambda n: b"Foo\nBar\nBaz\n"
             >>> t.recvline_pred(lambda line: line == b"Bar\n")
             b'Bar'
-            >>> t.recvline_pred(lambda line: line == b"Bar\n", keepends=True)
+            >>> t.recvline_pred(lambda line: line == b"Bar\n", True)
+            b'Bar\n'
+            >>> t.recvline_pred(lambda line: line == b"Bar\n", drop=False)
             b'Bar\n'
             >>> t.recvline_pred(lambda line: line == b'Nope!', timeout=0.1)
             b''
         """
+        drop = self._normalize_keepends_drop(keepends, drop, True)
+        del keepends
 
         tmpbuf = Buffer()
         line   = b''
         with self.countdown(timeout):
             while self.countdown_active():
                 try:
-                    line = self.recvline(keepends=True)
+                    line = self.recvline(drop=False)
                 except Exception:
                     self.buffer.unget(tmpbuf)
                     raise
@@ -574,22 +625,23 @@ def recvline_pred(self, pred, keepends=False, timeout=default):
                     return b''
 
                 if pred(line):
-                    if not keepends:
-                        line = line[:-len(self.newline)]
+                    if drop:
+                        line = line.rstrip(self.newline)
                     return line
                 else:
                     tmpbuf.add(line)
 
         return b''
 
-    def recvline_contains(self, items, keepends = False, timeout = default):
-        r"""
+    def recvline_contains(self, items, keepends=None, drop=None, timeout=default):
+        r"""recvline_contains(items, drop=True, timeout=default) -> bytes
+
         Receive lines until one line is found which contains at least
         one of `items`.
 
         Arguments:
             items(str,tuple): List of strings to search for, or a single string.
-            keepends(bool): Return lines with newlines if :const:`True`
+            drop(bool): Drop the line ending (:const:`True`).
             timeout(int): Timeout, in seconds
 
         Examples:
@@ -615,10 +667,10 @@ def recvline_contains(self, items, keepends = False, timeout = default):
         def pred(line):
             return any(d in line for d in items)
 
-        return self.recvline_pred(pred, keepends, timeout)
+        return self.recvline_pred(pred, keepends=keepends, drop=drop, timeout=timeout)
 
-    def recvline_startswith(self, delims, keepends=False, timeout=default):
-        r"""recvline_startswith(delims, keepends=False, timeout=default) -> bytes
+    def recvline_startswith(self, delims, keepends=None, drop=None, timeout=default):
+        r"""recvline_startswith(delims, drop=True, timeout=default) -> bytes
 
         Keep receiving lines until one is found that starts with one of
         `delims`.  Returns the last line received.
@@ -628,7 +680,7 @@ def recvline_startswith(self, delims, keepends=False, timeout=default):
 
         Arguments:
             delims(str,tuple): List of strings to search for, or string of single characters
-            keepends(bool): Return lines with newlines if :const:`True`
+            drop(bool): Drop the line ending (:const:`True`).
             timeout(int): Timeout, in seconds
 
         Returns:
@@ -640,7 +692,7 @@ def recvline_startswith(self, delims, keepends=False, timeout=default):
             >>> t.recv_raw = lambda n: b"Hello\nWorld\nXylophone\n"
             >>> t.recvline_startswith((b'W',b'X',b'Y',b'Z'))
             b'World'
-            >>> t.recvline_startswith((b'W',b'X',b'Y',b'Z'), True)
+            >>> t.recvline_startswith((b'W',b'X',b'Y',b'Z'), drop=False)
             b'Xylophone\n'
             >>> t.recvline_startswith(b'Wo')
             b'World'
@@ -652,10 +704,11 @@ def recvline_startswith(self, delims, keepends=False, timeout=default):
 
         return self.recvline_pred(lambda line: any(map(line.startswith, delims)),
                                   keepends=keepends,
+                                  drop=drop,
                                   timeout=timeout)
 
-    def recvline_endswith(self, delims, keepends=False, timeout=default):
-        r"""recvline_endswith(delims, keepends=False, timeout=default) -> bytes
+    def recvline_endswith(self, delims, keepends=None, drop=None, timeout=default):
+        r"""recvline_endswith(delims, drop=True, timeout=default) -> bytes
 
         Keep receiving lines until one is found that ends with one of
         `delims`.  Returns the last line received.
@@ -671,7 +724,7 @@ def recvline_endswith(self, delims, keepends=False, timeout=default):
             >>> t.recv_raw = lambda n: b'Foo\nBar\nBaz\nKaboodle\n'
             >>> t.recvline_endswith(b'r')
             b'Bar'
-            >>> t.recvline_endswith((b'a',b'b',b'c',b'd',b'e'), True)
+            >>> t.recvline_endswith((b'a',b'b',b'c',b'd',b'e'), drop=False)
             b'Kaboodle\n'
             >>> t.recvline_endswith(b'oodle')
             b'Kaboodle'
@@ -684,6 +737,7 @@ def recvline_endswith(self, delims, keepends=False, timeout=default):
 
         return self.recvline_pred(lambda line: any(map(line.endswith, delims)),
                                   keepends=keepends,
+                                  drop=drop,
                                   timeout=timeout)
 
     def recvregex(self, regex, exact=False, timeout=default, capture=False):
@@ -726,8 +780,8 @@ def recvregex(self, regex, exact=False, timeout=default, capture=False):
         else:
             return self.recvpred(pred, timeout = timeout)
 
-    def recvline_regex(self, regex, exact=False, keepends=False, timeout=default):
-        """recvline_regex(regex, exact=False, keepends=False, timeout=default) -> bytes
+    def recvline_regex(self, regex, exact=False, keepends=None, drop=None, timeout=default):
+        """recvline_regex(regex, exact=False, drop=True, timeout=default) -> bytes
 
         Wrapper around :func:`recvline_pred`, which will return when a regex
         matches a line.
@@ -748,7 +802,7 @@ def recvline_regex(self, regex, exact=False, keepends=False, timeout=default):
         else:
             pred = regex.search
 
-        return self.recvline_pred(pred, keepends = keepends, timeout = timeout)
+        return self.recvline_pred(pred, keepends=keepends, drop=drop, timeout=timeout)
 
     def recvrepeat(self, timeout=default):
         """recvrepeat(timeout=default) -> bytes
@@ -1064,8 +1118,8 @@ def clean_and_log(self, timeout = 0.05):
             >>> t.connected_raw = lambda d: True
             >>> t.fileno        = lambda: 1234
             >>> with context.local(log_level='info'):
-            ...     data = t.clean_and_log() #doctest: +ELLIPSIS
-            [DEBUG] Received 0xb bytes:
+            ...     data = t.clean_and_log()
+            [...] Received 0xb bytes:
                 b'hooray_data'
             >>> data
             b'hooray_data'
@@ -1440,7 +1494,7 @@ def shutdown(self, direction = "send"):
             send
             send
             send
-            >>> t.shutdown('bad_value') #doctest: +ELLIPSIS
+            >>> t.shutdown('bad_value')
             Traceback (most recent call last):
             ...
             KeyError: "direction must be in ['in', 'out', 'read', 'recv', 'send', 'write']"
@@ -1474,7 +1528,7 @@ def connected(self, direction = 'any'):
             send
             send
             send
-            >>> t.connected('bad_value') #doctest: +ELLIPSIS
+            >>> t.connected('bad_value')
             Traceback (most recent call last):
             ...
             KeyError: "direction must be in ['any', 'in', 'out', 'read', 'recv', 'send', 'write']"

From acdfe23651cefc4594c8d6db1c5567d0cf91fe6f Mon Sep 17 00:00:00 2001
From: MrQubo <jakub.jakub.nowak@gmail.com>
Date: Sat, 28 Sep 2024 23:37:55 +0200
Subject: [PATCH 2/3] changelog

---
 CHANGELOG.md | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 033971e16..dabf97a2d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -86,6 +86,7 @@ The table below shows which release corresponds to each branch, and what date th
 - [#2484][2484] Allow to disable caching
 - [#2291][2291] Fix attaching to a gdbserver with tuple `gdb.attach(('0.0.0.0',12345))`
 - [#2410][2410] Add `tube.upload_manually` to upload files in chunks
+- [#2476][2476] Deprecate 'keepends' argument in favor of 'drop'
 
 [2471]: https://github.com/Gallopsled/pwntools/pull/2471
 [2358]: https://github.com/Gallopsled/pwntools/pull/2358
@@ -100,6 +101,7 @@ The table below shows which release corresponds to each branch, and what date th
 [2484]: https://github.com/Gallopsled/pwntools/pull/2484
 [2291]: https://github.com/Gallopsled/pwntools/pull/2291
 [2410]: https://github.com/Gallopsled/pwntools/pull/2410
+[2476]: https://github.com/Gallopsled/pwntools/pull/2476
 
 ## 4.14.0 (`beta`)
 

From ee59abccc7e97d96755adf907155f410d04edf6c Mon Sep 17 00:00:00 2001
From: MrQubo <jakub.jakub.nowak@gmail.com>
Date: Thu, 3 Oct 2024 19:23:02 +0200
Subject: [PATCH 3/3] Ignore line numbers in PyLint check

---
 .github/workflows/pylint.yml | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml
index ee21b8614..410e1cae5 100644
--- a/.github/workflows/pylint.yml
+++ b/.github/workflows/pylint.yml
@@ -26,10 +26,11 @@ jobs:
         set -x
         pip install pylint
         pip install --upgrade -e .
-        pylint --exit-zero --errors-only pwnlib -f parseable | cut -d ' ' -f2- > current.txt
+        run_pylint() { pylint --exit-zero --errors-only pwnlib -f parseable | cut -d ' ' -f2- | sed 's/line [0-9]\+/line XXXX/g'; }
+        run_pylint > current.txt
         git fetch origin
         git checkout origin/"$GITHUB_BASE_REF"
-        pylint --exit-zero --errors-only pwnlib -f parseable | cut -d ' ' -f2- > base.txt
+        run_pylint > base.txt
         if diff base.txt current.txt | grep '>'; then
           false
         fi