Repository: hynek/structlog

Issue:
LogfmtRenderer  has a bug with escaped double quotes
In the code of LogFmtRenderer there is:

`value = f"{value}".replace('"', '\\"')`

However if you are trying to render an escaped double quote like
`"I want to render this \"string\" with logfmtrenderer"`

it ends up rendering it as
`\"I want to render this \\"string\\" with logfmtrenderer\"`

`\\"` is wrong and won't be parsed correctly by a logfmtparser.


Comments:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fd810de..9d3b8d5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -20,6 +20,9 @@ You can find our backwards-compatibility policy [here](https://github.com/hynek/
 - `structlog.processors.LogfmtRenderer` now escapes newlines.
   [#592](https://github.com/hynek/structlog/pull/592)
 
+- `structlog.processors.LogfmtRenderer` now escapes backslashes and double quotes.
+  [#594](https://github.com/hynek/structlog/pull/594)
+
 
 ## [24.1.0](https://github.com/hynek/structlog/compare/23.3.0...24.1.0) - 2024-01-08
 
diff --git a/src/structlog/processors.py b/src/structlog/processors.py
index ba87eb3..e597eae 100644
--- a/src/structlog/processors.py
+++ b/src/structlog/processors.py
@@ -170,9 +170,16 @@ class LogfmtRenderer:
                     continue
                 value = "true" if value else "false"
 
-            value = f"{value}".replace('"', '\\"').replace("\n", "\\n")
+            value = str(value)
+            backslashes_need_escaping = (
+                " " in value or "=" in value or '"' in value
+            )
+            if backslashes_need_escaping and "\\" in value:
+                value = value.replace("\\", "\\\\")
+
+            value = value.replace('"', '\\"').replace("\n", "\\n")
 
-            if " " in value or "=" in value:
+            if backslashes_need_escaping:
                 value = f'"{value}"'
 
             elements.append(f"{key}={value}")


Test patch:
diff --git a/tests/processors/test_renderers.py b/tests/processors/test_renderers.py
index b3d6e51..8f225c8 100644
--- a/tests/processors/test_renderers.py
+++ b/tests/processors/test_renderers.py
@@ -275,6 +275,33 @@ class TestLogfmtRenderer:
 
         assert r"with_newline=some\nvalue" == rv
 
+    @pytest.mark.parametrize(
+        ("raw", "escaped"),
+        [
+            # Slash by itself does not need to be escaped.
+            (r"a\slash", r"a\slash"),
+            # A quote requires quoting, and escaping the quote.
+            ('a"quote', r'"a\"quote"'),
+            # If anything triggers quoting of the string, then the slash must
+            # be escaped.
+            (
+                r'a\slash with space or a"quote',
+                r'"a\\slash with space or a\"quote"',
+            ),
+            (
+                r"I want to render this \"string\" with logfmtrenderer",
+                r'"I want to render this \\\"string\\\" with logfmtrenderer"',
+            ),
+        ],
+    )
+    def test_escaping(self, raw, escaped):
+        """
+        Backslashes and quotes are escaped.
+        """
+        rv = LogfmtRenderer()(None, None, {"key": raw})
+
+        assert f"key={escaped}" == rv
+
 
 class TestJSONRenderer:
     def test_renders_json(self, event_dict):