Repository: yt-dlp/yt-dlp

Issue:
`InfoExtractor._get_cookies` fails if values contain quotes
### DO NOT REMOVE OR SKIP THE ISSUE TEMPLATE

- [X] I understand that I will be **blocked** if I remove or skip any mandatory\* field

### Checklist

- [X] I'm reporting a bug unrelated to a specific site
- [X] I've verified that I'm running yt-dlp version **2022.08.19** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
- [X] I've checked that all provided URLs are playable in a browser with the same IP and same login details
- [X] I've checked that all URLs and arguments with special characters are [properly quoted or escaped](https://github.com/ytdl-org/youtube-dl#video-url-contains-an-ampersand-and-im-getting-some-strange-output-1-2839-or-v-is-not-recognized-as-an-internal-or-external-command)
- [X] I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues **including closed ones**. DO NOT post duplicates
- [X] I've read the [guidelines for opening an issue](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue)

### Provide a description that is worded well enough to be understood

`InfoExtractor._get_cookies` uses `http.cookies.SimpleCookie` to process the cookies. Analogue to #4692 the parsing will fail fast instead of skipping the invalid values.

SimpleCookie allows values with quotes if set explicitly.

### Provide verbose output that clearly demonstrates the problem

- [X] Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`)
- [X] Copy the WHOLE output (starting with `[debug] Command-line config`) and insert it below

### Complete Verbose Output

```shell
[debug] Command-line config: ['--cookies-from-browser', 'firefox', '-j', 'https://beta.crunchyroll.com/de/watch/GG1U2Q50J/the-former-couple-refuses-to-say', '-vU']
[debug] Encodings: locale cp1252, fs utf-8, pref cp1252, out utf-8, error utf-8, screen utf-8 
[debug] yt-dlp version 2022.08.19 [48c88e0] (pip) 
[debug] Python 3.10.5 (CPython 64bit) - Windows-10-10.0.22000-SP0  
[debug] Checking exe version: ffmpeg -bsfs 
[debug] Checking exe version: ffprobe -bsfs 
[debug] exe versions: ffmpeg n5.1-10-g6ee1996721-20220822 (setts), ffprobe n5.1-10-g6ee1996721-20220822 
[debug] Optional libraries: Cryptodome-3.15.0, brotli-1.0.9, certifi-2022.06.15, mutagen-1.45.1, sqlite3-2.6.0, websockets-10.3
[Cookies] Extracting cookies from firefox
[debug] Extracting cookies from: "C:\Users\grub4k\AppData\Roaming\Mozilla\Firefox\Profiles\kbiex092.default-release\cookies.sqlite"
[Cookies] Extracted 790 cookies from firefox
[debug] Proxy map: {} 
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest 
Latest version: 2022.08.19, Current version: 2022.08.19 
yt-dlp is up to date (2022.08.19) 
[debug] [crunchyroll:beta] Extracting URL: https://beta.crunchyroll.com/de/watch/GG1U2Q50J/the-former-couple-refuses-to-say
[crunchyroll:beta] the-former-couple-refuses-to-say: Downloading webpage 
ERROR: GG1U2Q50J: An extractor error has occurred. (caused by KeyError('byId')); please report this issue on  https://github.com/yt-dlp/yt-dlp/issues?q= , filling out the appropriate issue template. Confirm you are on the latest version using  yt-dlp -U
  File "C:\Users\grub4k\AppData\Local\pypoetry\Cache\virtualenvs\crunchyload-sk9Pq0JJ-py3.10\lib\site-packages\yt_dlp\extractor\common.py", line 666, in extract
    ie_result = self._real_extract(url)
  File "C:\Users\grub4k\AppData\Local\pypoetry\Cache\virtualenvs\crunchyload-sk9Pq0JJ-py3.10\lib\site-packages\yt_dlp\extractor\crunchyroll.py", line 805, in _real_extract
    return self._redirect_from_beta(url, lang, internal_id, display_id, True, CrunchyrollIE.ie_key())
  File "C:\Users\grub4k\AppData\Local\pypoetry\Cache\virtualenvs\crunchyload-sk9Pq0JJ-py3.10\lib\site-packages\yt_dlp\extractor\crunchyroll.py", line 752, in _redirect_from_beta
    content_data = initial_state['content']['byId'][internal_id]
KeyError: 'byId'
```



Discussion:
To clarify, it's only related to later discussion in 4692, specifically https://github.com/yt-dlp/yt-dlp/issues/4692#issuecomment-1222455567 and onward.
Another instance: https://github.com/yt-dlp/yt-dlp/issues/3778

Resolving patch:
diff --git a/yt_dlp/cookies.py b/yt_dlp/cookies.py
--- a/yt_dlp/cookies.py
+++ b/yt_dlp/cookies.py
@@ -1,6 +1,7 @@
 import base64
 import contextlib
 import http.cookiejar
+import http.cookies
 import json
 import os
 import re
@@ -989,3 +990,98 @@ def _parse_browser_specification(browser_name, profile=None, keyring=None, conta
     if profile is not None and _is_path(profile):
         profile = os.path.expanduser(profile)
     return browser_name, profile, keyring, container
+
+
+class LenientSimpleCookie(http.cookies.SimpleCookie):
+    """More lenient version of http.cookies.SimpleCookie"""
+    # From https://github.com/python/cpython/blob/v3.10.7/Lib/http/cookies.py
+    _LEGAL_KEY_CHARS = r"\w\d!#%&'~_`><@,:/\$\*\+\-\.\^\|\)\(\?\}\{\="
+    _LEGAL_VALUE_CHARS = _LEGAL_KEY_CHARS + r"\[\]"
+
+    _RESERVED = {
+        "expires",
+        "path",
+        "comment",
+        "domain",
+        "max-age",
+        "secure",
+        "httponly",
+        "version",
+        "samesite",
+    }
+
+    _FLAGS = {"secure", "httponly"}
+
+    # Added 'bad' group to catch the remaining value
+    _COOKIE_PATTERN = re.compile(r"""
+        \s*                            # Optional whitespace at start of cookie
+        (?P<key>                       # Start of group 'key'
+        [""" + _LEGAL_KEY_CHARS + r"""]+?# Any word of at least one letter
+        )                              # End of group 'key'
+        (                              # Optional group: there may not be a value.
+        \s*=\s*                          # Equal Sign
+        (                                # Start of potential value
+        (?P<val>                           # Start of group 'val'
+        "(?:[^\\"]|\\.)*"                    # Any doublequoted string
+        |                                    # or
+        \w{3},\s[\w\d\s-]{9,11}\s[\d:]{8}\sGMT # Special case for "expires" attr
+        |                                    # or
+        [""" + _LEGAL_VALUE_CHARS + r"""]*     # Any word or empty string
+        )                                  # End of group 'val'
+        |                                  # or
+        (?P<bad>(?:\\;|[^;])*?)            # 'bad' group fallback for invalid values
+        )                                # End of potential value
+        )?                             # End of optional value group
+        \s*                            # Any number of spaces.
+        (\s+|;|$)                      # Ending either at space, semicolon, or EOS.
+        """, re.ASCII | re.VERBOSE)
+
+    def load(self, data):
+        # Workaround for https://github.com/yt-dlp/yt-dlp/issues/4776
+        if not isinstance(data, str):
+            return super().load(data)
+
+        morsel = None
+        index = 0
+        length = len(data)
+
+        while 0 <= index < length:
+            match = self._COOKIE_PATTERN.search(data, index)
+            if not match:
+                break
+
+            index = match.end(0)
+            if match.group("bad"):
+                morsel = None
+                continue
+
+            key, value = match.group("key", "val")
+
+            if key[0] == "$":
+                if morsel is not None:
+                    morsel[key[1:]] = True
+                continue
+
+            lower_key = key.lower()
+            if lower_key in self._RESERVED:
+                if morsel is None:
+                    continue
+
+                if value is None:
+                    if lower_key not in self._FLAGS:
+                        morsel = None
+                        continue
+                    value = True
+                else:
+                    value, _ = self.value_decode(value)
+
+                morsel[key] = value
+
+            elif value is not None:
+                morsel = self.get(key, http.cookies.Morsel())
+                real_value, coded_value = self.value_decode(value)
+                morsel.set(key, real_value, coded_value)
+                self[key] = morsel
+
+            else:
+                morsel = None
diff --git a/yt_dlp/extractor/common.py b/yt_dlp/extractor/common.py
--- a/yt_dlp/extractor/common.py
+++ b/yt_dlp/extractor/common.py
@@ -21,6 +21,7 @@
 
 from ..compat import functools  # isort: split
 from ..compat import compat_etree_fromstring, compat_expanduser, compat_os_name
+from ..cookies import LenientSimpleCookie
 from ..downloader import FileDownloader
 from ..downloader.f4m import get_base_url, remove_encrypted_media
 from ..utils import (
@@ -3631,7 +3632,7 @@ def _set_cookie(self, domain, name, value, expire_time=None, port=None,
 
     def _get_cookies(self, url):
         """ Return a http.cookies.SimpleCookie with the cookies for the url """
-        return http.cookies.SimpleCookie(self._downloader._calc_cookies(url))
+        return LenientSimpleCookie(self._downloader._calc_cookies(url))
 
     def _apply_first_set_cookie_header(self, url_handle, cookie):
         """


Test patch:
diff --git a/test/test_cookies.py b/test/test_cookies.py
--- a/test/test_cookies.py
+++ b/test/test_cookies.py
@@ -3,6 +3,7 @@
 
 from yt_dlp import cookies
 from yt_dlp.cookies import (
+    LenientSimpleCookie,
     LinuxChromeCookieDecryptor,
     MacChromeCookieDecryptor,
     WindowsChromeCookieDecryptor,
@@ -137,3 +138,148 @@ def test_safari_cookie_parsing(self):
     def test_pbkdf2_sha1(self):
         key = pbkdf2_sha1(b'peanuts', b' ' * 16, 1, 16)
         self.assertEqual(key, b'g\xe1\x8e\x0fQ\x1c\x9b\xf3\xc9`!\xaa\x90\xd9\xd34')
+
+
+class TestLenientSimpleCookie(unittest.TestCase):
+    def _run_tests(self, *cases):
+        for message, raw_cookie, expected in cases:
+            cookie = LenientSimpleCookie(raw_cookie)
+
+            with self.subTest(message, expected=expected):
+                self.assertEqual(cookie.keys(), expected.keys(), message)
+
+                for key, expected_value in expected.items():
+                    morsel = cookie[key]
+                    if isinstance(expected_value, tuple):
+                        expected_value, expected_attributes = expected_value
+                    else:
+                        expected_attributes = {}
+
+                    attributes = {
+                        key: value
+                        for key, value in dict(morsel).items()
+                        if value != ""
+                    }
+                    self.assertEqual(attributes, expected_attributes, message)
+
+                    self.assertEqual(morsel.value, expected_value, message)
+
+    def test_parsing(self):
+        self._run_tests(
+            # Copied from https://github.com/python/cpython/blob/v3.10.7/Lib/test/test_http_cookies.py
+            (
+                "Test basic cookie",
+                "chips=ahoy; vienna=finger",
+                {"chips": "ahoy", "vienna": "finger"},
+            ),
+            (
+                "Test quoted cookie",
+                'keebler="E=mc2; L=\\"Loves\\"; fudge=\\012;"',
+                {"keebler": 'E=mc2; L="Loves"; fudge=\012;'},
+            ),
+            (
+                "Allow '=' in an unquoted value",
+                "keebler=E=mc2",
+                {"keebler": "E=mc2"},
+            ),
+            (
+                "Allow cookies with ':' in their name",
+                "key:term=value:term",
+                {"key:term": "value:term"},
+            ),
+            (
+                "Allow '[' and ']' in cookie values",
+                "a=b; c=[; d=r; f=h",
+                {"a": "b", "c": "[", "d": "r", "f": "h"},
+            ),
+            (
+                "Test basic cookie attributes",
+                'Customer="WILE_E_COYOTE"; Version=1; Path=/acme',
+                {"Customer": ("WILE_E_COYOTE", {"version": "1", "path": "/acme"})},
+            ),
+            (
+                "Test flag only cookie attributes",
+                'Customer="WILE_E_COYOTE"; HttpOnly; Secure',
+                {"Customer": ("WILE_E_COYOTE", {"httponly": True, "secure": True})},
+            ),
+            (
+                "Test flag only attribute with values",
+                "eggs=scrambled; httponly=foo; secure=bar; Path=/bacon",
+                {"eggs": ("scrambled", {"httponly": "foo", "secure": "bar", "path": "/bacon"})},
+            ),
+            (
+                "Test special case for 'expires' attribute, 4 digit year",
+                'Customer="W"; expires=Wed, 01 Jan 2010 00:00:00 GMT',
+                {"Customer": ("W", {"expires": "Wed, 01 Jan 2010 00:00:00 GMT"})},
+            ),
+            (
+                "Test special case for 'expires' attribute, 2 digit year",
+                'Customer="W"; expires=Wed, 01 Jan 98 00:00:00 GMT',
+                {"Customer": ("W", {"expires": "Wed, 01 Jan 98 00:00:00 GMT"})},
+            ),
+            (
+                "Test extra spaces in keys and values",
+                "eggs  =  scrambled  ;  secure  ;  path  =  bar   ; foo=foo   ",
+                {"eggs": ("scrambled", {"secure": True, "path": "bar"}), "foo": "foo"},
+            ),
+            (
+                "Test quoted attributes",
+                'Customer="WILE_E_COYOTE"; Version="1"; Path="/acme"',
+                {"Customer": ("WILE_E_COYOTE", {"version": "1", "path": "/acme"})}
+            ),
+            # Our own tests that CPython passes
+            (
+                "Allow ';' in quoted value",
+                'chips="a;hoy"; vienna=finger',
+                {"chips": "a;hoy", "vienna": "finger"},
+            ),
+            (
+                "Keep only the last set value",
+                "a=c; a=b",
+                {"a": "b"},
+            ),
+        )
+
+    def test_lenient_parsing(self):
+        self._run_tests(
+            (
+                "Ignore and try to skip invalid cookies",
+                'chips={"ahoy;": 1}; vienna="finger;"',
+                {"vienna": "finger;"},
+            ),
+            (
+                "Ignore cookies without a name",
+                "a=b; unnamed; c=d",
+                {"a": "b", "c": "d"},
+            ),
+            (
+                "Ignore '\"' cookie without name",
+                'a=b; "; c=d',
+                {"a": "b", "c": "d"},
+            ),
+            (
+                "Skip all space separated values",
+                "x a=b c=d x; e=f",
+                {"a": "b", "c": "d", "e": "f"},
+            ),
+            (
+                "Skip all space separated values",
+                'x a=b; data={"complex": "json", "with": "key=value"}; x c=d x',
+                {"a": "b", "c": "d"},
+            ),
+            (
+                "Expect quote mending",
+                'a=b; invalid="; c=d',
+                {"a": "b", "c": "d"},
+            ),
+            (
+                "Reset morsel after invalid to not capture attributes",
+                "a=b; invalid; Version=1; c=d",
+                {"a": "b", "c": "d"},
+            ),
+            (
+                "Continue after non-flag attribute without value",
+                "a=b; path; Version=1; c=d",
+                {"a": "b", "c": "d"},
+            ),
+        )