< Summary

Line coverage
0%
Covered lines: 0
Uncovered lines: 160
Coverable lines: 160
Total lines: 866
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 74
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Cyclomatic complexity NPath complexity Sequence coverage
.ctor(...)100%110%
InternalSetNameMethod(...)100%110%
Get()0%64640%
CheckQuoted(...)0%660%
EndofHeader()100%110%

File(s)

https://raw.githubusercontent.com/dotnet/runtime/811a7eabb75c42db53440e8ba3f60c07511cfd1f/src/libraries/Common/src/System/Net/CookieParser.cs

#LineLine coverage
 1// Licensed to the .NET Foundation under one or more agreements.
 2// The .NET Foundation licenses this file to you under the MIT license.
 3
 4using System.Diagnostics;
 5using System.Globalization;
 6using System.Reflection;
 7using System.Runtime.InteropServices;
 8
 9namespace System.Net
 10{
 11    internal enum CookieToken
 12    {
 13        // State types
 14        Nothing,
 15        NameValuePair,  // X=Y
 16        Attribute,      // X
 17        EndToken,       // ';'
 18        EndCookie,      // ','
 19        End,            // EOLN
 20        Equals,
 21
 22        // Value types
 23        Comment,
 24        CommentUrl,
 25        CookieName,
 26        Discard,
 27        Domain,
 28        Expires,
 29        MaxAge,
 30        Path,
 31        Port,
 32        Secure,
 33        HttpOnly,
 34        Unknown,
 35        Version
 36    }
 37
 38    // CookieTokenizer
 39    //
 40    // Used to split a single or multi-cookie (header) string into individual
 41    // tokens.
 42    internal struct CookieTokenizer
 43    {
 44        private bool _eofCookie;
 45        private int _index;
 46        private readonly int _length;
 47        private string? _name;
 48        private bool _quoted;
 49        private int _start;
 50        private CookieToken _token;
 51        private int _tokenLength;
 52        private readonly string _tokenStream;
 53        private string _value;
 54        private int _cookieStartIndex;
 55        private int _cookieLength;
 56
 57        internal CookieTokenizer(string tokenStream) : this()
 58        {
 59            _length = tokenStream.Length;
 60            _tokenStream = tokenStream;
 61            _value = string.Empty;
 62        }
 63
 64        internal bool EndOfCookie
 65        {
 66            get
 67            {
 68                return _eofCookie;
 69            }
 70            set
 71            {
 72                _eofCookie = value;
 73            }
 74        }
 75
 76        internal bool Eof
 77        {
 78            get
 79            {
 80                return _index >= _length;
 81            }
 82        }
 83
 84        internal string? Name
 85        {
 86            get
 87            {
 88                return _name;
 89            }
 90            set
 91            {
 92                _name = value;
 93            }
 94        }
 95
 96        internal bool Quoted
 97        {
 98            get
 99            {
 100                return _quoted;
 101            }
 102            set
 103            {
 104                _quoted = value;
 105            }
 106        }
 107
 108        internal CookieToken Token
 109        {
 110            get
 111            {
 112                return _token;
 113            }
 114            set
 115            {
 116                _token = value;
 117            }
 118        }
 119
 120        internal string Value
 121        {
 122            get
 123            {
 124                return _value;
 125            }
 126            set
 127            {
 128                _value = value;
 129            }
 130        }
 131
 132        // Extract
 133        //
 134        // Extracts the current token
 135        internal string Extract()
 136        {
 137            string tokenString = string.Empty;
 138
 139            if (_tokenLength != 0)
 140            {
 141                tokenString = Quoted ?
 142                    _tokenStream.Substring(_start, _tokenLength) :
 143                    _tokenStream.AsSpan(_start, _tokenLength).Trim().ToString();
 144            }
 145            return tokenString;
 146        }
 147
 148        // FindNext
 149        //
 150        // Find the start and length of the next token. The token is terminated
 151        // by one of:
 152        //     - end-of-line
 153        //     - end-of-cookie: unquoted comma separates multiple cookies
 154        //     - end-of-token: unquoted semi-colon
 155        //     - end-of-name: unquoted equals
 156        //
 157        // Inputs:
 158        // <argument>  ignoreComma
 159        //     true if parsing doesn't stop at a comma. This is only true when
 160        //     we know we're parsing an original cookie that has an expires=
 161        //     attribute, because the format of the time/date used in expires
 162        //     is:
 163        //         Wdy, dd-mmm-yyyy HH:MM:SS GMT
 164        //
 165        // <argument>  ignoreEquals
 166        //     true if parsing doesn't stop at an equals sign. The LHS of the
 167        //     first equals sign is an attribute name. The next token may
 168        //     include one or more equals signs. For example:
 169        //          SESSIONID=ID=MSNx45&q=33
 170        //
 171        // Outputs:
 172        // <member>    _index
 173        //     incremented to the last position in _tokenStream contained by
 174        //     the current token
 175        //
 176        // <member>    _start
 177        //     incremented to the start of the current token
 178        //
 179        // <member>    _tokenLength
 180        //     set to the length of the current token
 181        //
 182        // Assumes: Nothing
 183        //
 184        // Returns:
 185        // type of CookieToken found:
 186        //
 187        //     End         - end of the cookie string
 188        //     EndCookie   - end of current cookie in (potentially) a
 189        //                   multi-cookie string
 190        //     EndToken    - end of name=value pair, or end of an attribute
 191        //     Equals      - end of name=
 192        //
 193        // Throws: Nothing
 194        internal CookieToken FindNext(bool ignoreComma, bool ignoreEquals)
 195        {
 196            _tokenLength = 0;
 197            _start = _index;
 198            while ((_index < _length) && char.IsWhiteSpace(_tokenStream[_index]))
 199            {
 200                ++_index;
 201                ++_start;
 202            }
 203
 204            CookieToken token = CookieToken.End;
 205            int increment = 1;
 206
 207            if (!Eof)
 208            {
 209                if (_tokenStream[_index] == '"')
 210                {
 211                    Quoted = true;
 212                    ++_index;
 213                    bool quoteOn = false;
 214                    while (_index < _length)
 215                    {
 216                        char currChar = _tokenStream[_index];
 217                        if (!quoteOn && currChar == '"')
 218                        {
 219                            break;
 220                        }
 221
 222                        if (quoteOn)
 223                        {
 224                            quoteOn = false;
 225                        }
 226                        else if (currChar == '\\')
 227                        {
 228                            quoteOn = true;
 229                        }
 230                        ++_index;
 231                    }
 232                    if (_index < _length)
 233                    {
 234                        ++_index;
 235                    }
 236                    _tokenLength = _index - _start;
 237                    increment = 0;
 238                    // If we are here, reset ignoreComma.
 239                    // In effect, we ignore everything after quoted string until the next delimiter.
 240                    ignoreComma = false;
 241                }
 242                while ((_index < _length)
 243                       && (_tokenStream[_index] != ';')
 244                       && (ignoreEquals || (_tokenStream[_index] != '='))
 245                       && (ignoreComma || (_tokenStream[_index] != ',')))
 246                {
 247                    // Fixing 2 things:
 248                    // 1) ignore day of week in cookie string
 249                    // 2) revert ignoreComma once meet it, so won't miss the next cookie)
 250                    if (_tokenStream[_index] == ',')
 251                    {
 252                        _start = _index + 1;
 253                        _tokenLength = -1;
 254                        ignoreComma = false;
 255                    }
 256                    ++_index;
 257                    _tokenLength += increment;
 258                }
 259                if (!Eof)
 260                {
 261                    switch (_tokenStream[_index])
 262                    {
 263                        case ';':
 264                            token = CookieToken.EndToken;
 265                            break;
 266
 267                        case '=':
 268                            token = CookieToken.Equals;
 269                            break;
 270
 271                        default:
 272                            _cookieLength = _index - _cookieStartIndex;
 273                            token = CookieToken.EndCookie;
 274                            break;
 275                    }
 276                    ++_index;
 277                }
 278
 279                if (Eof)
 280                {
 281                    _cookieLength = _index - _cookieStartIndex;
 282                }
 283            }
 284            return token;
 285        }
 286
 287        // Next
 288        //
 289        // Get the next cookie name/value or attribute
 290        //
 291        // Cookies come in the following formats:
 292        //
 293        //     1. Version0
 294        //         Set-Cookie: [<name>][=][<value>]
 295        //                     [; expires=<date>]
 296        //                     [; path=<path>]
 297        //                     [; domain=<domain>]
 298        //                     [; secure]
 299        //         Cookie: <name>=<value>
 300        //
 301        //         Notes: <name> and/or <value> may be blank
 302        //                <date> is the RFC 822/1123 date format that
 303        //                incorporates commas, e.g.
 304        //                "Wednesday, 09-Nov-99 23:12:40 GMT"
 305        //
 306        //     2. RFC 2109
 307        //         Set-Cookie: 1#{
 308        //                         <name>=<value>
 309        //                         [; comment=<comment>]
 310        //                         [; domain=<domain>]
 311        //                         [; max-age=<seconds>]
 312        //                         [; path=<path>]
 313        //                         [; secure]
 314        //                         ; Version=<version>
 315        //                     }
 316        //         Cookie: $Version=<version>
 317        //                 1#{
 318        //                     ; <name>=<value>
 319        //                     [; path=<path>]
 320        //                     [; domain=<domain>]
 321        //                 }
 322        //
 323        //     3. RFC 2965
 324        //         Set-Cookie2: 1#{
 325        //                         <name>=<value>
 326        //                         [; comment=<comment>]
 327        //                         [; commentURL=<comment>]
 328        //                         [; discard]
 329        //                         [; domain=<domain>]
 330        //                         [; max-age=<seconds>]
 331        //                         [; path=<path>]
 332        //                         [; ports=<portlist>]
 333        //                         [; secure]
 334        //                         ; Version=<version>
 335        //                      }
 336        //         Cookie: $Version=<version>
 337        //                 1#{
 338        //                     ; <name>=<value>
 339        //                     [; path=<path>]
 340        //                     [; domain=<domain>]
 341        //                     [; port="<port>"]
 342        //                 }
 343        //         [Cookie2: $Version=<version>]
 344        //
 345        // Inputs:
 346        // <argument>  first
 347        //     true if this is the first name/attribute that we have looked for
 348        //     in the cookie stream
 349        //
 350        // Outputs:
 351        //
 352        // Assumes:
 353        // Nothing
 354        //
 355        // Returns:
 356        // type of CookieToken found:
 357        //
 358        //     - Attribute
 359        //         - token was single-value. May be empty. Caller should check
 360        //           Eof or EndCookie to determine if any more action needs to
 361        //           be taken
 362        //
 363        //     - NameValuePair
 364        //         - Name and Value are meaningful. Either may be empty
 365        //
 366        // Throws:
 367        // Nothing
 368        internal CookieToken Next(bool first, bool parseResponseCookies)
 369        {
 370            Reset();
 371
 372            if (first)
 373            {
 374                _cookieStartIndex = _index;
 375                _cookieLength = 0;
 376            }
 377
 378            CookieToken terminator = FindNext(false, false);
 379            if (terminator == CookieToken.EndCookie)
 380            {
 381                EndOfCookie = true;
 382            }
 383
 384            if ((terminator == CookieToken.End) || (terminator == CookieToken.EndCookie))
 385            {
 386                if ((Name = Extract()).Length != 0)
 387                {
 388                    Token = TokenFromName(parseResponseCookies);
 389                    return CookieToken.Attribute;
 390                }
 391                return terminator;
 392            }
 393            Name = Extract();
 394            if (first)
 395            {
 396                Token = CookieToken.CookieName;
 397            }
 398            else
 399            {
 400                Token = TokenFromName(parseResponseCookies);
 401            }
 402            if (terminator == CookieToken.Equals)
 403            {
 404                terminator = FindNext(!first && (Token == CookieToken.Expires), true);
 405                if (terminator == CookieToken.EndCookie)
 406                {
 407                    EndOfCookie = true;
 408                }
 409                Value = Extract();
 410                return CookieToken.NameValuePair;
 411            }
 412            else
 413            {
 414                return CookieToken.Attribute;
 415            }
 416        }
 417
 418        // Reset
 419        //
 420        // Sets this tokenizer up for finding the next name/value pair,
 421        // attribute, or end-of-{token,cookie,line}.
 422        internal void Reset()
 423        {
 424            _eofCookie = false;
 425            _name = string.Empty;
 426            _quoted = false;
 427            _start = _index;
 428            _token = CookieToken.Nothing;
 429            _tokenLength = 0;
 430            _value = string.Empty;
 431        }
 432
 433        private struct RecognizedAttribute
 434        {
 435            private readonly string _name;
 436            private readonly CookieToken _token;
 437
 438            internal RecognizedAttribute(string name, CookieToken token)
 439            {
 440                _name = name;
 441                _token = token;
 442            }
 443
 444            internal CookieToken Token
 445            {
 446                get
 447                {
 448                    return _token;
 449                }
 450            }
 451
 452            internal bool IsEqualTo(string? value)
 453            {
 454                return string.Equals(_name, value, StringComparison.OrdinalIgnoreCase);
 455            }
 456        }
 457
 458        // Recognized attributes in order of expected frequency.
 459        private static readonly RecognizedAttribute[] s_recognizedAttributes = {
 460            new RecognizedAttribute(CookieFields.PathAttributeName, CookieToken.Path),
 461            new RecognizedAttribute(CookieFields.MaxAgeAttributeName, CookieToken.MaxAge),
 462            new RecognizedAttribute(CookieFields.ExpiresAttributeName, CookieToken.Expires),
 463            new RecognizedAttribute(CookieFields.VersionAttributeName, CookieToken.Version),
 464            new RecognizedAttribute(CookieFields.DomainAttributeName, CookieToken.Domain),
 465            new RecognizedAttribute(CookieFields.SecureAttributeName, CookieToken.Secure),
 466            new RecognizedAttribute(CookieFields.DiscardAttributeName, CookieToken.Discard),
 467            new RecognizedAttribute(CookieFields.PortAttributeName, CookieToken.Port),
 468            new RecognizedAttribute(CookieFields.CommentAttributeName, CookieToken.Comment),
 469            new RecognizedAttribute(CookieFields.CommentUrlAttributeName, CookieToken.CommentUrl),
 470            new RecognizedAttribute(CookieFields.HttpOnlyAttributeName, CookieToken.HttpOnly),
 471        };
 472
 473        private static readonly RecognizedAttribute[] s_recognizedServerAttributes = {
 474            new RecognizedAttribute('$' + CookieFields.PathAttributeName, CookieToken.Path),
 475            new RecognizedAttribute('$' + CookieFields.VersionAttributeName, CookieToken.Version),
 476            new RecognizedAttribute('$' + CookieFields.DomainAttributeName, CookieToken.Domain),
 477            new RecognizedAttribute('$' + CookieFields.PortAttributeName, CookieToken.Port),
 478            new RecognizedAttribute('$' + CookieFields.HttpOnlyAttributeName, CookieToken.HttpOnly),
 479        };
 480
 481        internal CookieToken TokenFromName(bool parseResponseCookies)
 482        {
 483            if (!parseResponseCookies)
 484            {
 485                for (int i = 0; i < s_recognizedServerAttributes.Length; ++i)
 486                {
 487                    if (s_recognizedServerAttributes[i].IsEqualTo(Name))
 488                    {
 489                        return s_recognizedServerAttributes[i].Token;
 490                    }
 491                }
 492            }
 493            else
 494            {
 495                for (int i = 0; i < s_recognizedAttributes.Length; ++i)
 496                {
 497                    if (s_recognizedAttributes[i].IsEqualTo(Name))
 498                    {
 499                        return s_recognizedAttributes[i].Token;
 500                    }
 501                }
 502            }
 503            return CookieToken.Unknown;
 504        }
 505    }
 506
 507    // CookieParser
 508    //
 509    // Takes a cookie header, makes cookies.
 510    internal struct CookieParser
 511    {
 512        private CookieTokenizer _tokenizer;
 513        private Cookie? _savedCookie;
 514
 515        internal CookieParser(string cookieString)
 0516        {
 0517            _tokenizer = new CookieTokenizer(cookieString);
 0518            _savedCookie = null;
 0519        }
 520
 521#if SYSTEM_NET_PRIMITIVES_DLL
 522        private static bool InternalSetNameMethod(Cookie cookie, string? value)
 0523        {
 0524            return cookie.InternalSetName(value);
 0525        }
 526#else
 527        private static Func<Cookie, string?, bool>? s_internalSetNameMethod;
 528        private static Func<Cookie, string?, bool> InternalSetNameMethod
 529        {
 530            get
 531            {
 532                if (s_internalSetNameMethod == null)
 533                {
 534                    // TODO https://github.com/dotnet/runtime/issues/19348:
 535                    // We need to use Cookie.InternalSetName instead of the Cookie.set_Name wrapped in a try catch block
 536                    // Cookie.set_Name keeps the original name if the string is empty or null.
 537                    // Unfortunately this API is internal so we use reflection to access it. The method is cached for pe
 538                    MethodInfo? method = typeof(Cookie).GetMethod("InternalSetName", BindingFlags.Instance | BindingFlag
 539                    Debug.Assert(method != null, "We need to use an internal method named InternalSetName that is declar
 540                    s_internalSetNameMethod = (Func<Cookie, string?, bool>)Delegate.CreateDelegate(typeof(Func<Cookie, s
 541                }
 542
 543                return s_internalSetNameMethod;
 544            }
 545        }
 546#endif
 547
 548        private static FieldInfo? s_isQuotedDomainField;
 549        private static FieldInfo IsQuotedDomainField
 550        {
 551            get
 0552            {
 0553                if (s_isQuotedDomainField == null)
 0554                {
 555                    // TODO https://github.com/dotnet/runtime/issues/19348:
 0556                    FieldInfo? fieldInfo = typeof(Cookie).GetField("IsQuotedDomain", BindingFlags.Instance | BindingFlag
 0557                    Debug.Assert(fieldInfo != null, "We need to use an internal field named IsQuotedDomain that is decla
 0558                    s_isQuotedDomainField = fieldInfo;
 0559                }
 560
 0561                return s_isQuotedDomainField;
 0562            }
 563        }
 564
 565        private static FieldInfo? s_isQuotedVersionField;
 566        private static FieldInfo IsQuotedVersionField
 567        {
 568            get
 0569            {
 0570                if (s_isQuotedVersionField == null)
 0571                {
 572                    // TODO https://github.com/dotnet/runtime/issues/19348:
 0573                    FieldInfo? fieldInfo = typeof(Cookie).GetField("IsQuotedVersion", BindingFlags.Instance | BindingFla
 0574                    Debug.Assert(fieldInfo != null, "We need to use an internal field named IsQuotedVersion that is decl
 0575                    s_isQuotedVersionField = fieldInfo;
 0576                }
 577
 0578                return s_isQuotedVersionField;
 0579            }
 580        }
 581
 582        // Get
 583        //
 584        // Gets the next cookie or null if there are no more cookies.
 585        internal Cookie? Get()
 0586        {
 0587            Cookie? cookie = null;
 588
 589            // Only the first occurrence of an attribute value must be counted.
 0590            bool commentSet = false;
 0591            bool commentUriSet = false;
 0592            bool domainSet = false;
 0593            bool expiresSet = false;
 0594            bool pathSet = false;
 0595            bool portSet = false; // Special case: may have no value in header.
 0596            bool versionSet = false;
 0597            bool secureSet = false;
 0598            bool discardSet = false;
 599
 600            do
 0601            {
 0602                CookieToken token = _tokenizer.Next(cookie == null, true);
 0603                if (cookie == null && (token == CookieToken.NameValuePair || token == CookieToken.Attribute))
 0604                {
 0605                    cookie = new Cookie();
 0606                    InternalSetNameMethod(cookie, _tokenizer.Name);
 0607                    cookie.Value = _tokenizer.Value;
 0608                }
 609                else
 0610                {
 0611                    switch (token)
 612                    {
 613                        case CookieToken.NameValuePair:
 0614                            switch (_tokenizer.Token)
 615                            {
 616                                case CookieToken.Comment:
 0617                                    if (!commentSet)
 0618                                    {
 0619                                        commentSet = true;
 0620                                        cookie!.Comment = _tokenizer.Value;
 0621                                    }
 0622                                    break;
 623
 624                                case CookieToken.CommentUrl:
 0625                                    if (!commentUriSet)
 0626                                    {
 0627                                        commentUriSet = true;
 0628                                        if (Uri.TryCreate(CheckQuoted(_tokenizer.Value), UriKind.Absolute, out Uri? pars
 0629                                        {
 0630                                            cookie!.CommentUri = parsed;
 0631                                        }
 0632                                    }
 0633                                    break;
 634
 635                                case CookieToken.Domain:
 0636                                    if (!domainSet)
 0637                                    {
 0638                                        domainSet = true;
 0639                                        cookie!.Domain = CheckQuoted(_tokenizer.Value);
 0640                                        IsQuotedDomainField.SetValue(cookie, _tokenizer.Quoted);
 0641                                    }
 0642                                    break;
 643
 644                                case CookieToken.Expires:
 0645                                    if (!expiresSet)
 0646                                    {
 0647                                        expiresSet = true;
 648
 0649                                        if (DateTime.TryParse(CheckQuoted(_tokenizer.Value),
 0650                                            CultureInfo.InvariantCulture, DateTimeStyles.AllowWhiteSpaces | DateTimeStyl
 0651                                        {
 0652                                            cookie!.Expires = expires;
 0653                                        }
 654                                        else
 0655                                        {
 656                                            // This cookie will be rejected
 0657                                            InternalSetNameMethod(cookie!, string.Empty);
 0658                                        }
 0659                                    }
 0660                                    break;
 661
 662                                case CookieToken.MaxAge:
 0663                                    if (!expiresSet)
 0664                                    {
 0665                                        expiresSet = true;
 0666                                        if (int.TryParse(CheckQuoted(_tokenizer.Value), out int parsed))
 0667                                        {
 0668                                            cookie!.Expires = DateTime.UtcNow.AddSeconds(parsed);
 0669                                        }
 670                                        else
 0671                                        {
 672                                            // This cookie will be rejected
 0673                                            InternalSetNameMethod(cookie!, string.Empty);
 0674                                        }
 0675                                    }
 0676                                    break;
 677
 678                                case CookieToken.Path:
 0679                                    if (!pathSet)
 0680                                    {
 0681                                        pathSet = true;
 0682                                        cookie!.Path = _tokenizer.Value;
 0683                                    }
 0684                                    break;
 685
 686                                case CookieToken.Port:
 0687                                    if (!portSet)
 0688                                    {
 0689                                        portSet = true;
 690                                        try
 0691                                        {
 0692                                            cookie!.Port = _tokenizer.Value;
 0693                                        }
 0694                                        catch
 0695                                        {
 696                                            // This cookie will be rejected
 0697                                            InternalSetNameMethod(cookie!, string.Empty);
 0698                                        }
 0699                                    }
 0700                                    break;
 701
 702                                case CookieToken.Version:
 0703                                    if (!versionSet)
 0704                                    {
 0705                                        versionSet = true;
 706                                        int parsed;
 0707                                        if (int.TryParse(CheckQuoted(_tokenizer.Value), out parsed))
 0708                                        {
 0709                                            cookie!.Version = parsed;
 0710                                            IsQuotedVersionField.SetValue(cookie, _tokenizer.Quoted);
 0711                                        }
 712                                        else
 0713                                        {
 714                                            // This cookie will be rejected
 0715                                            InternalSetNameMethod(cookie!, string.Empty);
 0716                                        }
 0717                                    }
 0718                                    break;
 719                            }
 0720                            break;
 721
 722                        case CookieToken.Attribute:
 0723                            switch (_tokenizer.Token)
 724                            {
 725                                case CookieToken.Discard:
 0726                                    if (!discardSet)
 0727                                    {
 0728                                        discardSet = true;
 0729                                        cookie!.Discard = true;
 0730                                    }
 0731                                    break;
 732
 733                                case CookieToken.Secure:
 0734                                    if (!secureSet)
 0735                                    {
 0736                                        secureSet = true;
 0737                                        cookie!.Secure = true;
 0738                                    }
 0739                                    break;
 740
 741                                case CookieToken.HttpOnly:
 0742                                    cookie!.HttpOnly = true;
 0743                                    break;
 744
 745                                case CookieToken.Port:
 0746                                    if (!portSet)
 0747                                    {
 0748                                        portSet = true;
 0749                                        cookie!.Port = string.Empty;
 0750                                    }
 0751                                    break;
 752                            }
 0753                            break;
 754                    }
 0755                }
 0756            } while (!_tokenizer.Eof && !_tokenizer.EndOfCookie);
 757
 0758            return cookie;
 0759        }
 760
 761        internal Cookie? GetServer()
 762        {
 763            Cookie? cookie = _savedCookie;
 764            _savedCookie = null;
 765
 766            // Only the first occurrence of an attribute value must be counted.
 767            bool domainSet = false;
 768            bool pathSet = false;
 769            bool portSet = false; // Special case: may have no value in header.
 770
 771            do
 772            {
 773                bool first = cookie == null || string.IsNullOrEmpty(cookie.Name);
 774                CookieToken token = _tokenizer.Next(first, false);
 775
 776                if (first && (token == CookieToken.NameValuePair || token == CookieToken.Attribute))
 777                {
 778                    cookie ??= new Cookie();
 779                    InternalSetNameMethod(cookie, _tokenizer.Name);
 780                    cookie.Value = _tokenizer.Value;
 781                }
 782                else
 783                {
 784                    switch (token)
 785                    {
 786                        case CookieToken.NameValuePair:
 787                            switch (_tokenizer.Token)
 788                            {
 789                                case CookieToken.Domain:
 790                                    if (!domainSet)
 791                                    {
 792                                        domainSet = true;
 793                                        cookie!.Domain = CheckQuoted(_tokenizer.Value);
 794                                        IsQuotedDomainField.SetValue(cookie, _tokenizer.Quoted);
 795                                    }
 796                                    break;
 797
 798                                case CookieToken.Path:
 799                                    if (!pathSet)
 800                                    {
 801                                        pathSet = true;
 802                                        cookie!.Path = _tokenizer.Value;
 803                                    }
 804                                    break;
 805
 806                                case CookieToken.Port:
 807                                    if (!portSet)
 808                                    {
 809                                        portSet = true;
 810                                        try
 811                                        {
 812                                            cookie!.Port = _tokenizer.Value;
 813                                        }
 814                                        catch (CookieException)
 815                                        {
 816                                            // This cookie will be rejected
 817                                            InternalSetNameMethod(cookie!, string.Empty);
 818                                        }
 819                                    }
 820                                    break;
 821
 822                                case CookieToken.Version:
 823                                    // this is a new cookie, this token is for the next cookie.
 824                                    _savedCookie = new Cookie();
 825                                    if (int.TryParse(_tokenizer.Value, out int parsed))
 826                                    {
 827                                        _savedCookie.Version = parsed;
 828                                    }
 829                                    return cookie;
 830
 831                                case CookieToken.Unknown:
 832                                    // this is a new cookie, the token is for the next cookie.
 833                                    _savedCookie = new Cookie();
 834                                    InternalSetNameMethod(_savedCookie, _tokenizer.Name);
 835                                    _savedCookie.Value = _tokenizer.Value;
 836                                    return cookie;
 837                            }
 838                            break;
 839
 840                        case CookieToken.Attribute:
 841                            if (_tokenizer.Token == CookieToken.Port && !portSet)
 842                            {
 843                                portSet = true;
 844                                cookie!.Port = string.Empty;
 845                            }
 846                            break;
 847                    }
 848                }
 849            } while (!_tokenizer.Eof && !_tokenizer.EndOfCookie);
 850            return cookie;
 851        }
 852
 853        internal static string CheckQuoted(string value)
 0854        {
 0855            return (value.Length >= 2 && value.StartsWith('\"') && value.EndsWith('\"'))
 0856                ? value.Substring(1, value.Length - 2)
 0857                : value;
 0858        }
 859
 860        internal bool EndofHeader()
 0861        {
 0862            return _tokenizer.Eof;
 0863        }
 864    }
 865}
 866