< Summary

Line coverage
0%
Covered lines: 0
Uncovered lines: 221
Coverable lines: 221
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%
Extract()0%440%
FindNext(...)0%42420%
Next(...)0%18180%
Reset()100%110%
.ctor(...)100%110%
IsEqualTo(...)100%110%
.cctor()100%110%
TokenFromName(...)0%10100%

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
 057        internal CookieTokenizer(string tokenStream) : this()
 058        {
 059            _length = tokenStream.Length;
 060            _tokenStream = tokenStream;
 061            _value = string.Empty;
 062        }
 63
 64        internal bool EndOfCookie
 65        {
 66            get
 067            {
 068                return _eofCookie;
 069            }
 70            set
 071            {
 072                _eofCookie = value;
 073            }
 74        }
 75
 76        internal bool Eof
 77        {
 78            get
 079            {
 080                return _index >= _length;
 081            }
 82        }
 83
 84        internal string? Name
 85        {
 86            get
 087            {
 088                return _name;
 089            }
 90            set
 091            {
 092                _name = value;
 093            }
 94        }
 95
 96        internal bool Quoted
 97        {
 98            get
 099            {
 0100                return _quoted;
 0101            }
 102            set
 0103            {
 0104                _quoted = value;
 0105            }
 106        }
 107
 108        internal CookieToken Token
 109        {
 110            get
 0111            {
 0112                return _token;
 0113            }
 114            set
 0115            {
 0116                _token = value;
 0117            }
 118        }
 119
 120        internal string Value
 121        {
 122            get
 0123            {
 0124                return _value;
 0125            }
 126            set
 0127            {
 0128                _value = value;
 0129            }
 130        }
 131
 132        // Extract
 133        //
 134        // Extracts the current token
 135        internal string Extract()
 0136        {
 0137            string tokenString = string.Empty;
 138
 0139            if (_tokenLength != 0)
 0140            {
 0141                tokenString = Quoted ?
 0142                    _tokenStream.Substring(_start, _tokenLength) :
 0143                    _tokenStream.AsSpan(_start, _tokenLength).Trim().ToString();
 0144            }
 0145            return tokenString;
 0146        }
 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)
 0195        {
 0196            _tokenLength = 0;
 0197            _start = _index;
 0198            while ((_index < _length) && char.IsWhiteSpace(_tokenStream[_index]))
 0199            {
 0200                ++_index;
 0201                ++_start;
 0202            }
 203
 0204            CookieToken token = CookieToken.End;
 0205            int increment = 1;
 206
 0207            if (!Eof)
 0208            {
 0209                if (_tokenStream[_index] == '"')
 0210                {
 0211                    Quoted = true;
 0212                    ++_index;
 0213                    bool quoteOn = false;
 0214                    while (_index < _length)
 0215                    {
 0216                        char currChar = _tokenStream[_index];
 0217                        if (!quoteOn && currChar == '"')
 0218                        {
 0219                            break;
 220                        }
 221
 0222                        if (quoteOn)
 0223                        {
 0224                            quoteOn = false;
 0225                        }
 0226                        else if (currChar == '\\')
 0227                        {
 0228                            quoteOn = true;
 0229                        }
 0230                        ++_index;
 0231                    }
 0232                    if (_index < _length)
 0233                    {
 0234                        ++_index;
 0235                    }
 0236                    _tokenLength = _index - _start;
 0237                    increment = 0;
 238                    // If we are here, reset ignoreComma.
 239                    // In effect, we ignore everything after quoted string until the next delimiter.
 0240                    ignoreComma = false;
 0241                }
 0242                while ((_index < _length)
 0243                       && (_tokenStream[_index] != ';')
 0244                       && (ignoreEquals || (_tokenStream[_index] != '='))
 0245                       && (ignoreComma || (_tokenStream[_index] != ',')))
 0246                {
 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)
 0250                    if (_tokenStream[_index] == ',')
 0251                    {
 0252                        _start = _index + 1;
 0253                        _tokenLength = -1;
 0254                        ignoreComma = false;
 0255                    }
 0256                    ++_index;
 0257                    _tokenLength += increment;
 0258                }
 0259                if (!Eof)
 0260                {
 0261                    switch (_tokenStream[_index])
 262                    {
 263                        case ';':
 0264                            token = CookieToken.EndToken;
 0265                            break;
 266
 267                        case '=':
 0268                            token = CookieToken.Equals;
 0269                            break;
 270
 271                        default:
 0272                            _cookieLength = _index - _cookieStartIndex;
 0273                            token = CookieToken.EndCookie;
 0274                            break;
 275                    }
 0276                    ++_index;
 0277                }
 278
 0279                if (Eof)
 0280                {
 0281                    _cookieLength = _index - _cookieStartIndex;
 0282                }
 0283            }
 0284            return token;
 0285        }
 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)
 0369        {
 0370            Reset();
 371
 0372            if (first)
 0373            {
 0374                _cookieStartIndex = _index;
 0375                _cookieLength = 0;
 0376            }
 377
 0378            CookieToken terminator = FindNext(false, false);
 0379            if (terminator == CookieToken.EndCookie)
 0380            {
 0381                EndOfCookie = true;
 0382            }
 383
 0384            if ((terminator == CookieToken.End) || (terminator == CookieToken.EndCookie))
 0385            {
 0386                if ((Name = Extract()).Length != 0)
 0387                {
 0388                    Token = TokenFromName(parseResponseCookies);
 0389                    return CookieToken.Attribute;
 390                }
 0391                return terminator;
 392            }
 0393            Name = Extract();
 0394            if (first)
 0395            {
 0396                Token = CookieToken.CookieName;
 0397            }
 398            else
 0399            {
 0400                Token = TokenFromName(parseResponseCookies);
 0401            }
 0402            if (terminator == CookieToken.Equals)
 0403            {
 0404                terminator = FindNext(!first && (Token == CookieToken.Expires), true);
 0405                if (terminator == CookieToken.EndCookie)
 0406                {
 0407                    EndOfCookie = true;
 0408                }
 0409                Value = Extract();
 0410                return CookieToken.NameValuePair;
 411            }
 412            else
 0413            {
 0414                return CookieToken.Attribute;
 415            }
 0416        }
 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()
 0423        {
 0424            _eofCookie = false;
 0425            _name = string.Empty;
 0426            _quoted = false;
 0427            _start = _index;
 0428            _token = CookieToken.Nothing;
 0429            _tokenLength = 0;
 0430            _value = string.Empty;
 0431        }
 432
 433        private struct RecognizedAttribute
 434        {
 435            private readonly string _name;
 436            private readonly CookieToken _token;
 437
 438            internal RecognizedAttribute(string name, CookieToken token)
 0439            {
 0440                _name = name;
 0441                _token = token;
 0442            }
 443
 444            internal CookieToken Token
 445            {
 446                get
 0447                {
 0448                    return _token;
 0449                }
 450            }
 451
 452            internal bool IsEqualTo(string? value)
 0453            {
 0454                return string.Equals(_name, value, StringComparison.OrdinalIgnoreCase);
 0455            }
 456        }
 457
 458        // Recognized attributes in order of expected frequency.
 0459        private static readonly RecognizedAttribute[] s_recognizedAttributes = {
 0460            new RecognizedAttribute(CookieFields.PathAttributeName, CookieToken.Path),
 0461            new RecognizedAttribute(CookieFields.MaxAgeAttributeName, CookieToken.MaxAge),
 0462            new RecognizedAttribute(CookieFields.ExpiresAttributeName, CookieToken.Expires),
 0463            new RecognizedAttribute(CookieFields.VersionAttributeName, CookieToken.Version),
 0464            new RecognizedAttribute(CookieFields.DomainAttributeName, CookieToken.Domain),
 0465            new RecognizedAttribute(CookieFields.SecureAttributeName, CookieToken.Secure),
 0466            new RecognizedAttribute(CookieFields.DiscardAttributeName, CookieToken.Discard),
 0467            new RecognizedAttribute(CookieFields.PortAttributeName, CookieToken.Port),
 0468            new RecognizedAttribute(CookieFields.CommentAttributeName, CookieToken.Comment),
 0469            new RecognizedAttribute(CookieFields.CommentUrlAttributeName, CookieToken.CommentUrl),
 0470            new RecognizedAttribute(CookieFields.HttpOnlyAttributeName, CookieToken.HttpOnly),
 0471        };
 472
 0473        private static readonly RecognizedAttribute[] s_recognizedServerAttributes = {
 0474            new RecognizedAttribute('$' + CookieFields.PathAttributeName, CookieToken.Path),
 0475            new RecognizedAttribute('$' + CookieFields.VersionAttributeName, CookieToken.Version),
 0476            new RecognizedAttribute('$' + CookieFields.DomainAttributeName, CookieToken.Domain),
 0477            new RecognizedAttribute('$' + CookieFields.PortAttributeName, CookieToken.Port),
 0478            new RecognizedAttribute('$' + CookieFields.HttpOnlyAttributeName, CookieToken.HttpOnly),
 0479        };
 480
 481        internal CookieToken TokenFromName(bool parseResponseCookies)
 0482        {
 0483            if (!parseResponseCookies)
 0484            {
 0485                for (int i = 0; i < s_recognizedServerAttributes.Length; ++i)
 0486                {
 0487                    if (s_recognizedServerAttributes[i].IsEqualTo(Name))
 0488                    {
 0489                        return s_recognizedServerAttributes[i].Token;
 490                    }
 0491                }
 0492            }
 493            else
 0494            {
 0495                for (int i = 0; i < s_recognizedAttributes.Length; ++i)
 0496                {
 0497                    if (s_recognizedAttributes[i].IsEqualTo(Name))
 0498                    {
 0499                        return s_recognizedAttributes[i].Token;
 500                    }
 0501                }
 0502            }
 0503            return CookieToken.Unknown;
 0504        }
 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)
 516        {
 517            _tokenizer = new CookieTokenizer(cookieString);
 518            _savedCookie = null;
 519        }
 520
 521#if SYSTEM_NET_PRIMITIVES_DLL
 522        private static bool InternalSetNameMethod(Cookie cookie, string? value)
 523        {
 524            return cookie.InternalSetName(value);
 525        }
 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
 552            {
 553                if (s_isQuotedDomainField == null)
 554                {
 555                    // TODO https://github.com/dotnet/runtime/issues/19348:
 556                    FieldInfo? fieldInfo = typeof(Cookie).GetField("IsQuotedDomain", BindingFlags.Instance | BindingFlag
 557                    Debug.Assert(fieldInfo != null, "We need to use an internal field named IsQuotedDomain that is decla
 558                    s_isQuotedDomainField = fieldInfo;
 559                }
 560
 561                return s_isQuotedDomainField;
 562            }
 563        }
 564
 565        private static FieldInfo? s_isQuotedVersionField;
 566        private static FieldInfo IsQuotedVersionField
 567        {
 568            get
 569            {
 570                if (s_isQuotedVersionField == null)
 571                {
 572                    // TODO https://github.com/dotnet/runtime/issues/19348:
 573                    FieldInfo? fieldInfo = typeof(Cookie).GetField("IsQuotedVersion", BindingFlags.Instance | BindingFla
 574                    Debug.Assert(fieldInfo != null, "We need to use an internal field named IsQuotedVersion that is decl
 575                    s_isQuotedVersionField = fieldInfo;
 576                }
 577
 578                return s_isQuotedVersionField;
 579            }
 580        }
 581
 582        // Get
 583        //
 584        // Gets the next cookie or null if there are no more cookies.
 585        internal Cookie? Get()
 586        {
 587            Cookie? cookie = null;
 588
 589            // Only the first occurrence of an attribute value must be counted.
 590            bool commentSet = false;
 591            bool commentUriSet = false;
 592            bool domainSet = false;
 593            bool expiresSet = false;
 594            bool pathSet = false;
 595            bool portSet = false; // Special case: may have no value in header.
 596            bool versionSet = false;
 597            bool secureSet = false;
 598            bool discardSet = false;
 599
 600            do
 601            {
 602                CookieToken token = _tokenizer.Next(cookie == null, true);
 603                if (cookie == null && (token == CookieToken.NameValuePair || token == CookieToken.Attribute))
 604                {
 605                    cookie = new Cookie();
 606                    InternalSetNameMethod(cookie, _tokenizer.Name);
 607                    cookie.Value = _tokenizer.Value;
 608                }
 609                else
 610                {
 611                    switch (token)
 612                    {
 613                        case CookieToken.NameValuePair:
 614                            switch (_tokenizer.Token)
 615                            {
 616                                case CookieToken.Comment:
 617                                    if (!commentSet)
 618                                    {
 619                                        commentSet = true;
 620                                        cookie!.Comment = _tokenizer.Value;
 621                                    }
 622                                    break;
 623
 624                                case CookieToken.CommentUrl:
 625                                    if (!commentUriSet)
 626                                    {
 627                                        commentUriSet = true;
 628                                        if (Uri.TryCreate(CheckQuoted(_tokenizer.Value), UriKind.Absolute, out Uri? pars
 629                                        {
 630                                            cookie!.CommentUri = parsed;
 631                                        }
 632                                    }
 633                                    break;
 634
 635                                case CookieToken.Domain:
 636                                    if (!domainSet)
 637                                    {
 638                                        domainSet = true;
 639                                        cookie!.Domain = CheckQuoted(_tokenizer.Value);
 640                                        IsQuotedDomainField.SetValue(cookie, _tokenizer.Quoted);
 641                                    }
 642                                    break;
 643
 644                                case CookieToken.Expires:
 645                                    if (!expiresSet)
 646                                    {
 647                                        expiresSet = true;
 648
 649                                        if (DateTime.TryParse(CheckQuoted(_tokenizer.Value),
 650                                            CultureInfo.InvariantCulture, DateTimeStyles.AllowWhiteSpaces | DateTimeStyl
 651                                        {
 652                                            cookie!.Expires = expires;
 653                                        }
 654                                        else
 655                                        {
 656                                            // This cookie will be rejected
 657                                            InternalSetNameMethod(cookie!, string.Empty);
 658                                        }
 659                                    }
 660                                    break;
 661
 662                                case CookieToken.MaxAge:
 663                                    if (!expiresSet)
 664                                    {
 665                                        expiresSet = true;
 666                                        if (int.TryParse(CheckQuoted(_tokenizer.Value), out int parsed))
 667                                        {
 668                                            cookie!.Expires = DateTime.UtcNow.AddSeconds(parsed);
 669                                        }
 670                                        else
 671                                        {
 672                                            // This cookie will be rejected
 673                                            InternalSetNameMethod(cookie!, string.Empty);
 674                                        }
 675                                    }
 676                                    break;
 677
 678                                case CookieToken.Path:
 679                                    if (!pathSet)
 680                                    {
 681                                        pathSet = true;
 682                                        cookie!.Path = _tokenizer.Value;
 683                                    }
 684                                    break;
 685
 686                                case CookieToken.Port:
 687                                    if (!portSet)
 688                                    {
 689                                        portSet = true;
 690                                        try
 691                                        {
 692                                            cookie!.Port = _tokenizer.Value;
 693                                        }
 694                                        catch
 695                                        {
 696                                            // This cookie will be rejected
 697                                            InternalSetNameMethod(cookie!, string.Empty);
 698                                        }
 699                                    }
 700                                    break;
 701
 702                                case CookieToken.Version:
 703                                    if (!versionSet)
 704                                    {
 705                                        versionSet = true;
 706                                        int parsed;
 707                                        if (int.TryParse(CheckQuoted(_tokenizer.Value), out parsed))
 708                                        {
 709                                            cookie!.Version = parsed;
 710                                            IsQuotedVersionField.SetValue(cookie, _tokenizer.Quoted);
 711                                        }
 712                                        else
 713                                        {
 714                                            // This cookie will be rejected
 715                                            InternalSetNameMethod(cookie!, string.Empty);
 716                                        }
 717                                    }
 718                                    break;
 719                            }
 720                            break;
 721
 722                        case CookieToken.Attribute:
 723                            switch (_tokenizer.Token)
 724                            {
 725                                case CookieToken.Discard:
 726                                    if (!discardSet)
 727                                    {
 728                                        discardSet = true;
 729                                        cookie!.Discard = true;
 730                                    }
 731                                    break;
 732
 733                                case CookieToken.Secure:
 734                                    if (!secureSet)
 735                                    {
 736                                        secureSet = true;
 737                                        cookie!.Secure = true;
 738                                    }
 739                                    break;
 740
 741                                case CookieToken.HttpOnly:
 742                                    cookie!.HttpOnly = true;
 743                                    break;
 744
 745                                case CookieToken.Port:
 746                                    if (!portSet)
 747                                    {
 748                                        portSet = true;
 749                                        cookie!.Port = string.Empty;
 750                                    }
 751                                    break;
 752                            }
 753                            break;
 754                    }
 755                }
 756            } while (!_tokenizer.Eof && !_tokenizer.EndOfCookie);
 757
 758            return cookie;
 759        }
 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)
 854        {
 855            return (value.Length >= 2 && value.StartsWith('\"') && value.EndsWith('\"'))
 856                ? value.Substring(1, value.Length - 2)
 857                : value;
 858        }
 859
 860        internal bool EndofHeader()
 861        {
 862            return _tokenizer.Eof;
 863        }
 864    }
 865}
 866