From ab9d41e5309b417a3c7a84fa8f03cf4f93831f1b Mon Sep 17 00:00:00 2001 From: sebres Date: Fri, 14 Jun 2024 14:31:21 +0200 Subject: [PATCH] beautifier detect whether it can use unicode chars in stats table; asciified output of beautifier in test suite; closes gh-3750 --- fail2ban/client/beautifier.py | 51 ++++++++++++++-------- fail2ban/tests/clientbeautifiertestcase.py | 22 ++++++---- 2 files changed, 45 insertions(+), 28 deletions(-) diff --git a/fail2ban/client/beautifier.py b/fail2ban/client/beautifier.py index 7ef173a65..21c49b948 100644 --- a/fail2ban/client/beautifier.py +++ b/fail2ban/client/beautifier.py @@ -21,8 +21,10 @@ __author__ = "Cyril Jaquier, Yaroslav Halchenko" __copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2013- Yaroslav Halchenko" __license__ = "GPL" +import sys + from ..exceptions import UnknownJailException, DuplicateJailException -from ..helpers import getLogger, logging +from ..helpers import getLogger, logging, PREFER_ENC # Gets the instance of the logger. logSys = getLogger(__name__) @@ -36,6 +38,11 @@ logSys = getLogger(__name__) class Beautifier: + stdoutEnc = PREFER_ENC + if sys.stdout and sys.stdout.encoding is not None: + stdoutEnc = sys.stdout.encoding + encUtf = 1 if stdoutEnc.lower() == 'utf-8' else 0 + def __init__(self, cmd = None): self.__inputCmd = cmd @@ -104,7 +111,11 @@ class Beautifier: jail_stat(j, " " if i == len(jstat) else " | ") msg = "\n".join(msg) elif inC[0:1] == ['stats'] or inC[0:1] == ['statistics']: - def _statstable(response): + chrTable = [ + ['|', '-', '|', 'x', 'x', '-', '|', '-'], ## ascii + ["\u2551", "\u2550", "\u255F", "\u256B", "\u256C", "\u2569", "\u2502", "\u2500"] ## utf-8 + ]; + def _statstable(response, ct): tophead = ["Jail", "Backend", "Filter", "Actions"] headers = ["", "", "cur", "tot", "cur", "tot"] minlens = [8, 8, 3, 3, 3, 3] @@ -120,29 +131,31 @@ class Beautifier: f = "%%%ds" if ralign[i] else "%%-%ds" rfmt.append(f % lens[i]) hfmt.append(f % lens[i]) - rfmt = [rfmt[0], rfmt[1], "%s \u2502 %s" % (rfmt[2], rfmt[3]), "%s \u2502 %s" % (rfmt[4], rfmt[5])] - hfmt = [hfmt[0], hfmt[1], "%s \u2502 %s" % (hfmt[2], hfmt[3]), "%s \u2502 %s" % (hfmt[4], hfmt[5])] + rfmt = [rfmt[0], rfmt[1], "%s %s %s" % (rfmt[2], ct[6], rfmt[3]), "%s %s %s" % (rfmt[4], ct[6], rfmt[5])] + hfmt = [hfmt[0], hfmt[1], "%s %s %s" % (hfmt[2], ct[6], hfmt[3]), "%s %s %s" % (hfmt[4], ct[6], hfmt[5])] tlens = [lens[0], lens[1], 3 + lens[2] + lens[3], 3 + lens[4] + lens[5]] tfmt = [hfmt[0], hfmt[1], "%%-%ds" % (tlens[2],), "%%-%ds" % (tlens[3],)] tsep = tfmt[0:2] - rfmt = " \u2551 ".join(rfmt) - hfmt = " \u2551 ".join(hfmt) - tfmt = " \u2551 ".join(tfmt) - tsep = " \u2551 ".join(tsep) - separator = ((tsep % tuple(tophead[0:2])) + " \u255F\u2500" + - ("\u2500\u256B\u2500".join(['\u2500' * n for n in tlens[2:]])) + '\u2500') + rfmt = (" "+ct[0]+" ").join(rfmt) + hfmt = (" "+ct[0]+" ").join(hfmt) + tfmt = (" "+ct[0]+" ").join(tfmt) + tsep = (" "+ct[0]+" ").join(tsep) + separator = ((tsep % tuple(tophead[0:2])) + " "+ct[2]+ct[7] + + ((ct[7]+ct[3]+ct[7]).join([ct[7] * n for n in tlens[2:]])) + ct[7]) ret = [] - ret.append(tfmt % tuple(["", ""]+tophead[2:])) - ret.append(separator) - ret.append(hfmt % tuple(headers)) - separator = "\u2550\u256C\u2550".join(['\u2550' * n for n in tlens]) + '\u2550' - ret.append(separator) + ret.append(" "+tfmt % tuple(["", ""]+tophead[2:])) + ret.append(" "+separator) + ret.append(" "+hfmt % tuple(headers)) + separator = (ct[1]+ct[4]+ct[1]).join([ct[1] * n for n in tlens]) + ct[1] + ret.append(ct[1]+separator) for row in rows: - ret.append(rfmt % tuple(row)) - separator = "\u2550\u2569\u2550".join(['\u2550' * n for n in tlens]) + '\u2550' - ret.append(separator) + ret.append(" "+rfmt % tuple(row)) + separator = (ct[1]+ct[5]+ct[1]).join([ct[1] * n for n in tlens]) + ct[1] + ret.append(ct[1]+separator) return ret - msg = "\n".join(_statstable(response)) + if not response: + return "No jails found." + msg = "\n".join(_statstable(response, chrTable[self.encUtf])) elif len(inC) < 2: pass # to few cmd args for below elif inC[1] == "syslogsocket": diff --git a/fail2ban/tests/clientbeautifiertestcase.py b/fail2ban/tests/clientbeautifiertestcase.py index defedbe1b..5fcb24047 100644 --- a/fail2ban/tests/clientbeautifiertestcase.py +++ b/fail2ban/tests/clientbeautifiertestcase.py @@ -34,6 +34,7 @@ class BeautifierTest(unittest.TestCase): """ Call before every test case """ super(BeautifierTest, self).setUp() self.b = Beautifier() + self.b.encUtf = 0; ## we prefer ascii in test suite (see #3750) def tearDown(self): """ Call after every test case """ @@ -170,22 +171,25 @@ class BeautifierTest(unittest.TestCase): def testStatusStats(self): self.b.setInputCmd(["stats"]) + ## no jails: + self.assertEqual(self.b.beautify({}), "No jails found.") + ## 3 jails: response = { "ssh": ["systemd", (3, 6), (12, 24)], "exim4": ["pyinotify", (6, 12), (20, 20)], "jail-with-long-name": ["polling", (0, 0), (0, 0)] } output = ("" - + " ? ? Filter ? Actions \n" - + "Jail ? Backend ????????????????????????\n" - + " ? ? cur ? tot ? cur ? tot\n" - + "????????????????????????????????????????????????????????\n" - + "ssh ? systemd ? 3 ? 6 ? 12 ? 24\n" - + "exim4 ? pyinotify ? 6 ? 12 ? 20 ? 20\n" - + "jail-with-long-name ? polling ? 0 ? 0 ? 0 ? 0\n" - + "????????????????????????????????????????????????????????" + + " | | Filter | Actions \n" + + " Jail | Backend |-----------x-----------\n" + + " | | cur | tot | cur | tot\n" + + "---------------------x-----------x-----------x-----------\n" + + " ssh | systemd | 3 | 6 | 12 | 24\n" + + " exim4 | pyinotify | 6 | 12 | 20 | 20\n" + + " jail-with-long-name | polling | 0 | 0 | 0 | 0\n" + + "---------------------------------------------------------" ) - response = self.b.beautify(response).encode('ascii', 'replace').decode('ascii') + response = self.b.beautify(response) self.assertEqual(response, output)