text: 'use strict';
  
  // Enough of https://github.com/salesforce/tough-cookie/blob/a928b54f6d08fc7bb966be5199fc2753e402348a/lib/cookie.js for a cookie-parsing implementation.
  var DATE_DELIM = /[\x09\x20-\x2F\x3B-\x40\x5B-\x60\x7B-\x7E]/;
  
  var DAY_OF_MONTH = /^(\d{1,2})[^\d]*$/;
  var TIME = /^(\d{1,2})[^\d]*:(\d{1,2})[^\d]*:(\d{1,2})[^\d]*$/;
  var MONTH = /^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)/i;
  
  var MONTH_TO_NUM = {
    jan:0, feb:1, mar:2, apr:3, may:4, jun:5,
    jul:6, aug:7, sep:8, oct:9, nov:10, dec:11
  };
  
  var YEAR = /^(\d{2}|\d{4})$/; // 2 to 4 digits
  
  // Double quotes are part of the value (see: S4.1.1).
  // '\r', '\n' and '\0' should be treated as a terminator in the "relaxed" mode
  // (see: https://github.com/ChromiumWebApps/chromium/blob/b3d3b4da8bb94c1b2e061600df106d590fda3620/net/cookies/parsed_cookie.cc#L60)
  // '=' and ';' are attribute/values separators
  // (see: https://github.com/ChromiumWebApps/chromium/blob/b3d3b4da8bb94c1b2e061600df106d590fda3620/net/cookies/parsed_cookie.cc#L64)
  var COOKIE_PAIR = /^(([^=;]+))\s*=\s*([^\n\r\0]*)/;
  
  // Used to parse non-RFC-compliant cookies like '=abc' when given the `loose`
  // option in Cookie.parse:
  // XXX Parse loose cookie pairs?
  var LOOSE_COOKIE_PAIR = /^((?:=)?([^=;]*)\s*=\s*)?([^\n\r\0]*)/;
  
  var CONTROL_CHARS = /[\x00-\x1F]/;
  
  // RFC6265 S5.1.1 date parser:
  function parseDate(str) {
    if (!str) {
      return;
    }
  
    /* RFC6265 S5.1.1:
     * 2. Process each date-token sequentially in the order the date-tokens
     * appear in the cookie-date
     */
    var tokens = str.split(DATE_DELIM);
    if (!tokens) {
      return;
    }
  
    var hour = null;
    var minutes = null;
    var seconds = null;
    var day = null;
    var month = null;
    var year = null;
  
    for (var i=0; i<tokens.length; i++) {
      var token = tokens[i].trim();
      if (!token.length) {
        continue;
      }
  
      var result;
  
      /* 2.1. If the found-time flag is not set and the token matches the time
       * production, set the found-time flag and set the hour- value,
       * minute-value, and second-value to the numbers denoted by the digits in
       * the date-token, respectively.  Skip the remaining sub-steps and continue
       * to the next date-token.
       */
      if (seconds === null) {
        result = TIME.exec(token);
        if (result) {
          hour = parseInt(result[1], 10);
          minutes = parseInt(result[2], 10);
          seconds = parseInt(result[3], 10);
          /* RFC6265 S5.1.1.5:
           * [fail if]
           * *  the hour-value is greater than 23,
           * *  the minute-value is greater than 59, or
           * *  the second-value is greater than 59.
           */
          if(hour > 23 || minutes > 59 || seconds > 59) {
            return;
          }
  
          continue;
        }
      }
  
      /* 2.2. If the found-day-of-month flag is not set and the date-token matches
       * the day-of-month production, set the found-day-of- month flag and set
       * the day-of-month-value to the number denoted by the date-token.  Skip
       * the remaining sub-steps and continue to the next date-token.
       */
      if (day === null) {
        result = DAY_OF_MONTH.exec(token);
        if (result) {
          day = parseInt(result, 10);
          /* RFC6265 S5.1.1.5:
           * [fail if] the day-of-month-value is less than 1 or greater than 31
           */
          if(day < 1 || day > 31) {
            return;
          }
          continue;
        }
      }
  
      /* 2.3. If the found-month flag is not set and the date-token matches the
       * month production, set the found-month flag and set the month-value to
       * the month denoted by the date-token.  Skip the remaining sub-steps and
       * continue to the next date-token.
       */
      if (month === null) {
        result = MONTH.exec(token);
        if (result) {
          month = MONTH_TO_NUM[result[1].toLowerCase()];
          continue;
        }
      }
  
      /* 2.4. If the found-year flag is not set and the date-token matches the year
       * production, set the found-year flag and set the year-value to the number
       * denoted by the date-token.  Skip the remaining sub-steps and continue to
       * the next date-token.
       */
      if (year === null) {
        result = YEAR.exec(token);
        if (result) {
          year = parseInt(result[0], 10);
          /* From S5.1.1:
           * 3.  If the year-value is greater than or equal to 70 and less
           * than or equal to 99, increment the year-value by 1900.
           * 4.  If the year-value is greater than or equal to 0 and less
           * than or equal to 69, increment the year-value by 2000.
           */
          if (70 <= year && year <= 99) {
            year += 1900;
          } else if (0 <= year && year <= 69) {
            year += 2000;
          }
  
          if (year < 1601) {
            return; // 5. ... the year-value is less than 1601
          }
        }
      }
    }
  
    if (seconds === null || day === null || month === null || year === null) {
      return; // 5. ... at least one of the found-day-of-month, found-month, found-
              // year, or found-time flags is not set,
    }
  
    return new Date(Date.UTC(year, month, day, hour, minutes, seconds));
  }
  
  function Cookie(options) {
    options = options || {};
  
    Object.keys(options).forEach(function(prop) {
      if (Cookie.prototype.hasOwnProperty(prop) &&
          Cookie.prototype[prop] !== options[prop] &&
          prop.substr(0,1) !== '_')
      {
        this[prop] = options[prop];
      }
    }, this);
  
    this.creation = this.creation || new Date();
  
    // used to break creation ties in cookieCompare():
    Object.defineProperty(this, 'creationIndex', {
      configurable: false,
      enumerable: false, // important for assert.deepEqual checks
      writable: true,
      value: ++Cookie.cookiesCreated
    });
  }
  
  Cookie.cookiesCreated = 0; // incremented each time a cookie is created
  
  Cookie.prototype.setMaxAge = function setMaxAge(age) {
    if (age === Infinity || age === -Infinity) {
      this.maxAge = age.toString(); // so JSON.stringify() works
    } else {
      this.maxAge = age;
    }
  };
  
  function parse(str, options) {
    if (!options || typeof options !== 'object') {
      options = {};
    }
    str = str.trim();
  
    // We use a regex to parse the "name-value-pair" part of S5.2
    var firstSemi = str.indexOf(';'); // S5.2 step 1
    var pairRe = options.loose ? LOOSE_COOKIE_PAIR : COOKIE_PAIR;
    var result = pairRe.exec(firstSemi === -1 ? str : str.substr(0,firstSemi));
  
    // Rx satisfies the "the name string is empty" and "lacks a %x3D ("=")"
    // constraints as well as trimming any whitespace.
    if (!result) {
      return;
    }
  
    var c = new Cookie();
    if (result[1]) {
      c.key = result[2].trim();
    } else {
      c.key = '';
    }
    c.value = result[3].trim();
    if (CONTROL_CHARS.test(c.key) || CONTROL_CHARS.test(c.value)) {
      return;
    }
  
    if (firstSemi === -1) {
      return c;
    }
  
    // S5.2.3 "unparsed-attributes consist of the remainder of the set-cookie-string
    // (including the %x3B (";") in question)." plus later on in the same section
    // "discard the first ";" and trim".
    var unparsed = str.slice(firstSemi + 1).trim();
  
    // "If the unparsed-attributes string is empty, skip the rest of these
    // steps."
    if (unparsed.length === 0) {
      return c;
    }
  
    /*
     * S5.2 says that when looping over the items "[p]rocess the attribute-name
     * and attribute-value according to the requirements in the following
     * subsections" for every item.  Plus, for many of the individual attributes
     * in S5.3 it says to use the "attribute-value of the last attribute in the
     * cookie-attribute-list".  Therefore, in this implementation, we overwrite
     * the previous value.
     */
    var cookie_avs = unparsed.split(';');
    while (cookie_avs.length) {
      var av = cookie_avs.shift().trim();
      if (av.length === 0) { // happens if ";;" appears
        continue;
      }
      var av_sep = av.indexOf('=');
      var av_key, av_value;
  
      if (av_sep === -1) {
        av_key = av;
        av_value = null;
      } else {
        av_key = av.substr(0,av_sep);
        av_value = av.substr(av_sep+1);
      }
  
      av_key = av_key.trim().toLowerCase();
  
      if (av_value) {
        av_value = av_value.trim();
      }
  
      switch(av_key) {
      case 'expires': // S5.2.1
        if (av_value) {
          var exp = parseDate(av_value);
          // "If the attribute-value failed to parse as a cookie date, ignore the
          // cookie-av."
          if (exp) {
            // over and underflow not realistically a concern: V8's getTime() seems to
            // store something larger than a 32-bit time_t (even with 32-bit node)
            c.expires = exp;
          }
        }
        break;
  
      case 'max-age': // S5.2.2
        if (av_value) {
          // "If the first character of the attribute-value is not a DIGIT or a "-"
          // character ...[or]... If the remainder of attribute-value contains a
          // non-DIGIT character, ignore the cookie-av."
          if (/^-?[0-9]+$/.test(av_value)) {
            var delta = parseInt(av_value, 10);
            // "If delta-seconds is less than or equal to zero (0), let expiry-time
            // be the earliest representable date and time."
            c.setMaxAge(delta);
          }
        }
        break;
  
      case 'domain': // S5.2.3
        // "If the attribute-value is empty, the behavior is undefined.  However,
        // the user agent SHOULD ignore the cookie-av entirely."
        if (av_value) {
          // S5.2.3 "Let cookie-domain be the attribute-value without the leading %x2E
          // (".") character."
          var domain = av_value.trim().replace(/^\./, '');
          if (domain) {
            // "Convert the cookie-domain to lower case."
            c.domain = domain.toLowerCase();
          }
        }
        break;
  
      case 'path': // S5.2.4
        /*
         * "If the attribute-value is empty or if the first character of the
         * attribute-value is not %x2F ("/"):
         *   Let cookie-path be the default-path.
         * Otherwise:
         *   Let cookie-path be the attribute-value."
         *
         * We'll represent the default-path as null since it depends on the
         * context of the parsing.
         */
        c.path = av_value && av_value[0] === "/" ? av_value : null;
        break;
  
      case 'secure': // S5.2.5
        /*
         * "If the attribute-name case-insensitively matches the string "Secure",
         * the user agent MUST append an attribute to the cookie-attribute-list
         * with an attribute-name of Secure and an empty attribute-value."
         */
        c.secure = true;
        break;
  
      case 'httponly': // S5.2.6 -- effectively the same as 'secure'
        c.httpOnly = true;
        break;
  
      default:
        c.extensions = c.extensions || [];
        c.extensions.push(av);
        break;
      }
    }
  
    return c;
  }
  
  // XXX Eventually give this a pure PureScript implementation.
  function parseImpl(cookieString) {
  	return parse(cookieString);
  }
  
  exports.parseImpl = parseImpl;
  