Bug 610054 - Clean up MIME header parsing (allow different behavior for HTTP and EMail); r=bz
authorjulian.reschke@gmx.de
Mon, 10 Oct 2011 15:27:05 +0100
changeset 78426 68b7ba48300450a22f1c33bc02b21fd59ab89a3b
parent 78425 6e5cf287ab1e4b5928cda73a0308562e7d28257e
child 78427 fc4e4fa98b14b4d0fa6f84ab72c52a8f040eac3d
push id2538
push userbmo@edmorley.co.uk
push dateMon, 10 Oct 2011 14:28:06 +0000
treeherdermozilla-inbound@be7e806933a4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbz
bugs610054
milestone10.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 610054 - Clean up MIME header parsing (allow different behavior for HTTP and EMail); r=bz
netwerk/mime/nsIMIMEHeaderParam.idl
netwerk/mime/nsMIMEHeaderParamImpl.cpp
netwerk/mime/nsMIMEHeaderParamImpl.h
netwerk/test/unit/test_MIME_params.js
--- a/netwerk/mime/nsIMIMEHeaderParam.idl
+++ b/netwerk/mime/nsIMIMEHeaderParam.idl
@@ -34,17 +34,17 @@
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
  
 /*
  * This interface allows any module to access the routine 
- * for MIME header parameter parsing (RFC 2231)
+ * for MIME header parameter parsing (RFC 2231/5987)
  */
 
 #include "nsISupports.idl"
 
 [scriptable, uuid(ddbbdfb8-a1c0-4dd5-a31b-5d2a7a3bb6ec)]
 interface nsIMIMEHeaderParam : nsISupports {
 
   /** 
@@ -92,16 +92,28 @@ interface nsIMIMEHeaderParam : nsISuppor
    * @return the value of <code>aParamName</code> in Unichar(UTF-16).
    */
 
   AString getParameter(in ACString aHeaderVal,
                        in string aParamName,
                        in ACString aFallbackCharset,
                        in boolean aTryLocaleCharset, 
                        out string aLang);
+
+
+  /**
+   * Like getParameter, but using RFC 5987 instead of 2231.  This removes
+   * support for header parameter continuations (foo*0, foo*1, etc).
+   */
+  AString getParameter5987(in ACString aHeaderVal,
+                           in string aParamName,
+                           in ACString aFallbackCharset,
+                           in boolean aTryLocaleCharset, 
+                           out string aLang);
+
   /** 
    * Given the value of a single header field  (such as
    * Content-Disposition and Content-Type) and the name of a parameter
    * (e.g. filename, name, charset), returns the value of the parameter 
    * after decoding RFC 2231-style encoding. 
    * <p>
    * For <strong>internal use only</strong>. The only other place where 
    * this needs to be  invoked  is  |MimeHeaders_get_parameter| in 
--- a/netwerk/mime/nsMIMEHeaderParamImpl.cpp
+++ b/netwerk/mime/nsMIMEHeaderParamImpl.cpp
@@ -70,33 +70,57 @@ static nsresult DecodeRFC2047Str(const c
 // low, but in theory it's possible. 
 #define IS_7BIT_NON_ASCII_CHARSET(cset)            \
     (!nsCRT::strncasecmp((cset), "ISO-2022", 8) || \
      !nsCRT::strncasecmp((cset), "HZ-GB", 5)    || \
      !nsCRT::strncasecmp((cset), "UTF-7", 5))   
 
 NS_IMPL_ISUPPORTS1(nsMIMEHeaderParamImpl, nsIMIMEHeaderParam)
 
-// XXX : aTryLocaleCharset is not yet effective.
 NS_IMETHODIMP 
 nsMIMEHeaderParamImpl::GetParameter(const nsACString& aHeaderVal, 
                                     const char *aParamName,
                                     const nsACString& aFallbackCharset, 
                                     bool aTryLocaleCharset, 
                                     char **aLang, nsAString& aResult)
 {
+  return DoGetParameter(aHeaderVal, aParamName, RFC_2231_DECODING,
+                        aFallbackCharset, aTryLocaleCharset, aLang, aResult);
+}
+
+NS_IMETHODIMP 
+nsMIMEHeaderParamImpl::GetParameter5987(const nsACString& aHeaderVal, 
+                                        const char *aParamName,
+                                        const nsACString& aFallbackCharset, 
+                                        bool aTryLocaleCharset, 
+                                        char **aLang, nsAString& aResult)
+{
+  return DoGetParameter(aHeaderVal, aParamName, RFC_5987_DECODING,
+                        aFallbackCharset, aTryLocaleCharset, aLang, aResult);
+}
+
+// XXX : aTryLocaleCharset is not yet effective.
+nsresult 
+nsMIMEHeaderParamImpl::DoGetParameter(const nsACString& aHeaderVal, 
+                                      const char *aParamName,
+                                      ParamDecoding aDecoding,
+                                      const nsACString& aFallbackCharset, 
+                                      bool aTryLocaleCharset, 
+                                      char **aLang, nsAString& aResult)
+{
     aResult.Truncate();
     nsresult rv;
 
-    // get parameter (decode RFC 2231 if it's RFC 2231-encoded and 
-    // return charset.)
+    // get parameter (decode RFC 2231/5987 when applicable, as specified by
+    // aDecoding (5987 being a subset of 2231) and return charset.)
     nsXPIDLCString med;
     nsXPIDLCString charset;
-    rv = GetParameterInternal(PromiseFlatCString(aHeaderVal).get(), aParamName, 
-                              getter_Copies(charset), aLang, getter_Copies(med));
+    rv = DoParameterInternal(PromiseFlatCString(aHeaderVal).get(), aParamName, 
+                             aDecoding, getter_Copies(charset), aLang, 
+                             getter_Copies(med));
     if (NS_FAILED(rv))
         return rv; 
 
     // convert to UTF-8 after charset conversion and RFC 2047 decoding 
     // if necessary.
     
     nsCAutoString str1;
     rv = DecodeParameter(med, charset.get(), nsnull, PR_FALSE, str1);
@@ -154,16 +178,30 @@ void RemoveQuotedStringEscapes(char *src
 // <token> [ ';' <token> '=' <token-or-quoted-string> ]*
 NS_IMETHODIMP 
 nsMIMEHeaderParamImpl::GetParameterInternal(const char *aHeaderValue, 
                                             const char *aParamName,
                                             char **aCharset,
                                             char **aLang,
                                             char **aResult)
 {
+  return DoParameterInternal(aHeaderValue, aParamName, RFC_2231_DECODING,
+                             aCharset, aLang, aResult);
+}
+
+
+nsresult 
+nsMIMEHeaderParamImpl::DoParameterInternal(const char *aHeaderValue, 
+                                           const char *aParamName,
+                                           ParamDecoding aDecoding,
+                                           char **aCharset,
+                                           char **aLang,
+                                           char **aResult)
+{
+
   if (!aHeaderValue ||  !*aHeaderValue || !aResult)
     return NS_ERROR_INVALID_ARG;
 
   *aResult = nsnull;
 
   if (aCharset) *aCharset = nsnull;
   if (aLang) *aLang = nsnull;
 
@@ -210,18 +248,22 @@ nsMIMEHeaderParamImpl::GetParameterInter
   // RFC2231 - The legitimate parm format can be:
   // A. title=ThisIsTitle 
   // B. title*=us-ascii'en-us'This%20is%20wierd.
   // C. title*0*=us-ascii'en'This%20is%20wierd.%20We
   //    title*1*=have%20to%20support%20this.
   //    title*2="Else..."
   // D. title*0="Hey, what you think you are doing?"
   //    title*1="There is no charset and lang info."
+  // RFC5987: only A and B
+  
+  PRInt32 paramLen = strlen(aParamName);
 
-  PRInt32 paramLen = strlen(aParamName);
+  bool haveCaseAValue = false;
+  PRInt32 nextContinuation = 0; // next value in series, or -1 if error
 
   while (*str) {
     const char *tokenStart = str;
     const char *tokenEnd = 0;
     const char *valueStart = str;
     const char *valueEnd = 0;
     bool seenEquals = false;
 
@@ -271,44 +313,63 @@ nsMIMEHeaderParamImpl::GetParameterInter
 
     // See if this is the simplest case (case A above),
     // a 'single' line value with no charset and lang.
     // If so, copy it and return.
     if (tokenEnd - tokenStart == paramLen &&
         seenEquals &&
         !nsCRT::strncasecmp(tokenStart, aParamName, paramLen))
     {
+      if (*aResult)
+      {
+        // either seen earlier caseA value already--we prefer first--or caseA
+        // came after a continuation: either way, prefer other value
+        goto increment_str;
+      }
       // if the parameter spans across multiple lines we have to strip out the
       //     line continuation -- jht 4/29/98 
       nsCAutoString tempStr(valueStart, valueEnd - valueStart);
       tempStr.StripChars("\r\n");
       char *res = ToNewCString(tempStr);
       NS_ENSURE_TRUE(res, NS_ERROR_OUT_OF_MEMORY);
       
       if (needUnquote)
         RemoveQuotedStringEscapes(res);
             
       *aResult = res;
       
-      // keep going, we may find a RFC 2231 encoded alternative
+      haveCaseAValue = true;
+      // keep going, we may find a RFC 2231/5987 encoded alternative
     }
     // case B, C, and D
     else if (tokenEnd - tokenStart > paramLen &&
              !nsCRT::strncasecmp(tokenStart, aParamName, paramLen) &&
              seenEquals &&
              *(tokenStart + paramLen) == '*')
     {
-      const char *cp = tokenStart + paramLen + 1; // 1st char pass '*'
+      const char *cp = tokenStart + paramLen + 1; // 1st char past '*'
       bool needUnescape = *(tokenEnd - 1) == '*';
-      // the 1st line of a multi-line parameter or a single line  that needs 
-      // unescaping. ( title*0*=  or  title*= )
-      // only allowed for token form, not for quoted-string
-      if (!needUnquote &&
-          ((*cp == '0' && needUnescape) || (tokenEnd - tokenStart == paramLen + 1)))
+
+      bool caseB = (tokenEnd - tokenStart) == paramLen + 1;
+      bool caseCorDStart = (*cp == '0') && needUnescape;
+      bool acceptContinuations = (aDecoding != RFC_5987_DECODING);
+ 
+      // CaseB and start of CaseC: requires charset and optional language
+      // in quotes (quotes required even if lang is blank)
+      if (!needUnquote && (caseB || (caseCorDStart && acceptContinuations)))
       {
+        if (caseCorDStart) {
+          if (nextContinuation++ != 0)
+          {
+            // error: already started a continuation.  Skip future
+            // continuations and return whatever initial parts were in order.
+            nextContinuation = -1;
+            goto increment_str;
+          }
+        }
         // look for single quotation mark(')
         const char *sQuote1 = PL_strchr(valueStart, 0x27);
         const char *sQuote2 = (char *) (sQuote1 ? PL_strchr(sQuote1 + 1, 0x27) : nsnull);
 
         // Two single quotation marks must be present even in
         // absence of charset and lang. 
         if (!sQuote1 || !sQuote2)
           NS_WARNING("Mandatory two single quotes are missing in header parameter\n");
@@ -335,71 +396,90 @@ nsMIMEHeaderParamImpl::GetParameterInter
         }
         else
           sQuote2 = valueStart - 1;
 
         if (sQuote2 && sQuote2 + 1 < valueEnd)
         {
           if (*aResult)
           {
-            // drop non-2231-encoded value, instead prefer the one using
-            // the RFC2231 encoding
+            // caseA value already read, or caseC/D value already read
+            // but we're now reading caseB: either way, drop old value
             nsMemory::Free(*aResult);
+            haveCaseAValue = false;
           }
           *aResult = (char *) nsMemory::Alloc(valueEnd - (sQuote2 + 1) + 1);
           if (*aResult)
           {
             memcpy(*aResult, sQuote2 + 1, valueEnd - (sQuote2 + 1));
             *(*aResult + (valueEnd - (sQuote2 + 1))) = 0;
             if (needUnescape)
             {
               nsUnescape(*aResult);
-              if (tokenEnd - tokenStart == paramLen + 1)
-                // we're done; this is case B 
-                return NS_OK; 
+              if (caseB)
+                return NS_OK; // caseB wins over everything else
             }
           }
         }
       }  // end of if-block :  title*0*=  or  title*= 
-      // a line of multiline param with no need for unescaping : title*[0-9]=
-      // or 2nd or later lines of a multiline param : title*[1-9]*= 
-      else if (nsCRT::IsAsciiDigit(PRUnichar(*cp)))
+      // caseD: a line of multiline param with no need for unescaping : title*[0-9]=
+      // or 2nd or later lines of a caseC param : title*[1-9]*= 
+      else if (acceptContinuations && nsCRT::IsAsciiDigit(PRUnichar(*cp)))
       {
+        PRInt32 nextSegment = atoi(cp);
+        // no leading zeros allowed except for ... position 0
+        bool broken = nextSegment > 0 && *cp == '0';
+          
+        if (broken || nextSegment != nextContinuation++)
+        {
+          // error: gap in continuation or unneccessary leading 0.
+          // Skip future continuations and return whatever initial parts were
+          // in order.
+          nextContinuation = -1;
+          goto increment_str;
+        }
+        if (haveCaseAValue && *aResult) 
+        {
+          // drop caseA value
+          nsMemory::Free(*aResult);
+          *aResult = 0;
+          haveCaseAValue = false;
+        }
         PRInt32 len = 0;
         if (*aResult) // 2nd or later lines of multiline parameter
         {
           len = strlen(*aResult);
           char *ns = (char *) nsMemory::Realloc(*aResult, len + (valueEnd - valueStart) + 1);
           if (!ns)
           {
             nsMemory::Free(*aResult);
           }
           *aResult = ns;
         }
-        else if (*cp == '0') // must be; 1st line :  title*0=
+        else 
         {
+          NS_ASSERTION(*cp == '0', "Not first value in continuation"); // must be; 1st line :  title*0=
           *aResult = (char *) nsMemory::Alloc(valueEnd - valueStart + 1);
         }
-        // else {} something is really wrong; out of memory
         if (*aResult)
         {
           // append a partial value
           memcpy(*aResult + len, valueStart, valueEnd - valueStart);
           *(*aResult + len + (valueEnd - valueStart)) = 0;
           if (needUnescape)
             nsUnescape(*aResult + len);
         }
         else 
           return NS_ERROR_OUT_OF_MEMORY;
       } // end of if-block :  title*[0-9]= or title*[1-9]*=
     }
 
     // str now points after the end of the value.
     //   skip over whitespace, ';', whitespace.
-      
+increment_str:      
     while (nsCRT::IsAsciiSpace(*str)) ++str;
     if (*str == ';') ++str;
     while (nsCRT::IsAsciiSpace(*str)) ++str;
   }
 
   if (*aResult) 
     return NS_OK;
   else
@@ -420,18 +500,18 @@ nsMIMEHeaderParamImpl::DecodeRFC2047Head
   if (!*aHeaderVal)
     return NS_OK;
 
 
   // If aHeaderVal is RFC 2047 encoded or is not a UTF-8 string  but
   // aDefaultCharset is specified, decodes RFC 2047 encoding and converts
   // to UTF-8. Otherwise, just strips away CRLF. 
   if (PL_strstr(aHeaderVal, "=?") || 
-      aDefaultCharset && (!IsUTF8(nsDependentCString(aHeaderVal)) || 
-      Is7bitNonAsciiString(aHeaderVal, PL_strlen(aHeaderVal)))) {
+      (aDefaultCharset && (!IsUTF8(nsDependentCString(aHeaderVal)) || 
+      Is7bitNonAsciiString(aHeaderVal, PL_strlen(aHeaderVal))))) {
     DecodeRFC2047Str(aHeaderVal, aDefaultCharset, aOverrideCharset, aResult);
   } else if (aEatContinuations && 
              (PL_strchr(aHeaderVal, '\n') || PL_strchr(aHeaderVal, '\r'))) {
     aResult = aHeaderVal;
   } else {
     aEatContinuations = PR_FALSE;
     aResult = aHeaderVal;
   }
@@ -450,17 +530,17 @@ nsMIMEHeaderParamImpl::DecodeRFC2047Head
 NS_IMETHODIMP 
 nsMIMEHeaderParamImpl::DecodeParameter(const nsACString& aParamValue,
                                        const char* aCharset,
                                        const char* aDefaultCharset,
                                        bool aOverrideCharset, 
                                        nsACString& aResult)
 {
   aResult.Truncate();
-  // If aCharset is given, aParamValue was obtained from RFC2231 
+  // If aCharset is given, aParamValue was obtained from RFC2231/5987 
   // encoding and we're pretty sure that it's in aCharset.
   if (aCharset && *aCharset)
   {
     nsCOMPtr<nsIUTF8ConverterService> cvtUTF8(do_GetService(NS_UTF8CONVERTERSERVICE_CONTRACTID));
     if (cvtUTF8)
       // skip ASCIIness/UTF8ness test if aCharset is 7bit non-ascii charset.
       return cvtUTF8->ConvertStringToUTF8(aParamValue, aCharset,
           IS_7BIT_NON_ASCII_CHARSET(aCharset), aResult);
@@ -496,19 +576,19 @@ nsMIMEHeaderParamImpl::DecodeParameter(c
   
   if (NS_SUCCEEDED(rv) && !decoded.IsEmpty())
     aResult = decoded;
   
   return rv;
 }
 
 #define ISHEXCHAR(c) \
-        (0x30 <= PRUint8(c) && PRUint8(c) <= 0x39  ||  \
-         0x41 <= PRUint8(c) && PRUint8(c) <= 0x46  ||  \
-         0x61 <= PRUint8(c) && PRUint8(c) <= 0x66)
+        ((0x30 <= PRUint8(c) && PRUint8(c) <= 0x39)  ||  \
+         (0x41 <= PRUint8(c) && PRUint8(c) <= 0x46)  ||  \
+         (0x61 <= PRUint8(c) && PRUint8(c) <= 0x66))
 
 // Decode Q encoding (RFC 2047).
 // static
 char *DecodeQ(const char *in, PRUint32 length)
 {
   char *out, *dest = 0;
 
   out = dest = (char *)PR_Calloc(length + 1, sizeof(char));
--- a/netwerk/mime/nsMIMEHeaderParamImpl.h
+++ b/netwerk/mime/nsMIMEHeaderParamImpl.h
@@ -42,12 +42,35 @@
 class nsMIMEHeaderParamImpl : public nsIMIMEHeaderParam
 {
 public:
   NS_DECL_ISUPPORTS
   NS_DECL_NSIMIMEHEADERPARAM
 
   nsMIMEHeaderParamImpl() {}
   virtual ~nsMIMEHeaderParamImpl() {}
+private:
+  // Toggles support for RFC 2231 decoding, or RFC 5987 (5987 profiles 2231
+  // for use in HTTP, and, for instance, drops support for continuations)
+  enum ParamDecoding {
+    RFC_2231_DECODING = 1,
+    RFC_5987_DECODING
+  }; 
+
+  nsresult DoGetParameter(const nsACString& aHeaderVal, 
+                          const char *aParamName,
+                          ParamDecoding aDecoding,
+                          const nsACString& aFallbackCharset, 
+                          bool aTryLocaleCharset, 
+                          char **aLang, 
+                          nsAString& aResult);
+
+  nsresult DoParameterInternal(const char *aHeaderValue, 
+                               const char *aParamName,
+                               ParamDecoding aDecoding,
+                               char **aCharset,
+                               char **aLang,
+                               char **aResult);
+
 };
 
 #endif 
 
--- a/netwerk/test/unit/test_MIME_params.js
+++ b/netwerk/test/unit/test_MIME_params.js
@@ -1,111 +1,190 @@
 /**
  * Test for bug 588781: ensure that for a MIME parameter with both "filename"
  * and "filename*" we pick the RFC2231/5987 encoded form
  *
  * Note: RFC 5987 drops support for continuations (foo*0, foo*1*, etc.). 
  *
  * See also <http://greenbytes.de/tech/webdav/rfc5987.html#rfc.section.4.2>
  */
+const Cr = Components.results;
 
-var succeed = [
-  ["Content-Disposition: attachment; filename=basic; filename*=UTF-8''extended",
-  "extended"],
-  ["Content-Disposition: attachment; filename*=UTF-8''extended; filename=basic",
-  "extended"],
-  ["Content-Disposition: attachment; filename=basic",
-  "basic"],
-  ["Content-Disposition: attachment; filename*=UTF-8''extended",
-  "extended"],
-  ["Content-Disposition: attachment; filename*0=foo; filename*1=bar",
-  "foobar"],
-/* BROKEN: we prepend 'basic' to result
-    ["Content-Disposition: attachment; filename=basic; filename*0=foo; filename*1=bar",
-    "foobar"],
-*/
+// Test array:
+//  - element 0: "Content-Disposition" header to test for 'filename' parameter
+//  - element 1: correct value returned under RFC 2231 (email)
+//  - element 2: correct value returned under RFC 5987 (HTTP)
+
+var tests = [
+  // No filename parameter: return nothing
+  ["attachment;", 
+    Cr.NS_ERROR_INVALID_ARG, Cr.NS_ERROR_INVALID_ARG],
+
+  // basic
+  ["attachment; filename=basic",
+   "basic", "basic"],
+
+  // extended
+  ["attachment; filename*=UTF-8''extended",
+   "extended", "extended"],
+
+  // prefer extended to basic
+  ["attachment; filename=basic; filename*=UTF-8''extended",
+   "extended", "extended"],
+
+  // prefer extended to basic
+  ["attachment; filename*=UTF-8''extended; filename=basic",
+   "extended", "extended"],
+
+  // use first basic value 
+  ["attachment; filename=first; filename=wrong",
+   "first", "first"],
+
+  // old school bad HTTP servers: missing 'attachment' or 'inline'
+  ["filename=old",
+   "old", "old"],
+
+  ["attachment; filename*=UTF-8''extended",
+   "extended", "extended"],
+
+  ["attachment; filename*0=foo; filename*1=bar",
+   "foobar", Cr.NS_ERROR_INVALID_ARG],
+
+  // Return first continuation
+  ["attachment; filename*0=first; filename*0=wrong; filename=basic",
+   "first", "basic"],
+
+  // Only use correctly ordered continuations
+  ["attachment; filename*0=first; filename*1=second; filename*0=wrong",
+   "firstsecond", Cr.NS_ERROR_INVALID_ARG],
+
+  // prefer continuation to basic (unless RFC 5987)
+  ["attachment; filename=basic; filename*0=foo; filename*1=bar",
+   "foobar", "basic"],
+
+  // Prefer extended to basic and/or (broken or not) continuation
+  ["attachment; filename=basic; filename*0=first; filename*0=wrong; filename*=UTF-8''extended",
+   "extended", "extended"],
+
   // RFC 2231 not clear on correct outcome: we prefer non-continued extended
-  ["Content-Disposition: attachment; filename=basic; filename*=UTF-8''extended; filename*0=foo; filename*1=bar",
-  "extended"],
-/* BROKEN: not checking order yet 
+  ["attachment; filename=basic; filename*=UTF-8''extended; filename*0=foo; filename*1=bar",
+   "extended", "extended"],
+
   // Gaps should result in returning only value until gap hit
-  ["Content-Disposition: attachment; filename*0=foo; filename*2=bar",
-  "foo"],
-*/
-/* BROKEN: don't check for leading 0s yet
-  // Don't handle leading 0's (*01)
-  ["Content-Disposition: attachment; filename*0=foo; filename*01=bar",
-  "foo"],
-*/
-  ["Content-Disposition: attachment; filename=basic; filename*0*=UTF-8''multi\r\n"
+  ["attachment; filename*0=foo; filename*2=bar",
+   "foo", Cr.NS_ERROR_INVALID_ARG],
+
+  // Don't allow leading 0's (*01)
+  ["attachment; filename*0=foo; filename*01=bar",
+   "foo", Cr.NS_ERROR_INVALID_ARG],
+
+  // continuations should prevail over non-extended (unless RFC 5987)
+  ["attachment; filename=basic; filename*0*=UTF-8''multi\r\n"
     + " filename*1=line\r\n" 
     + " filename*2*=%20extended",
-  "multiline extended"],
-/* BROKEN: not checking order yet 
+   "multiline extended", "basic"],
+
   // Gaps should result in returning only value until gap hit
-  ["Content-Disposition: attachment; filename=basic; filename*0*=UTF-8''multi\r\n"
+  ["attachment; filename=basic; filename*0*=UTF-8''multi\r\n"
     + " filename*1=line\r\n" 
     + " filename*3*=%20extended",
-  "multiline"],
-*/
+   "multiline", "basic"],
+
+  // First series, only please, and don't slurp up higher elements (*2 in this
+  // case) from later series into earlier one
+  ["attachment; filename=basic; filename*0*=UTF-8''multi\r\n"
+    + " filename*1=line\r\n" 
+    + " filename*0*=UTF-8''wrong\r\n"
+    + " filename*1=bad\r\n"
+    + " filename*2=evil",
+   "multiline", "basic"],
+
   // RFC 2231 not clear on correct outcome: we prefer non-continued extended
-  ["Content-Disposition: attachment; filename=basic; filename*0=UTF-8''multi\r\n"
+  ["attachment; filename=basic; filename*0=UTF-8''multi\r\n"
     + " filename*=UTF-8''extended\r\n"
     + " filename*1=line\r\n" 
     + " filename*2*=%20extended",
-  "extended"],
-  // sneaky: if unescaped, make sure we leave UTF-8'' in value
-  ["Content-Disposition: attachment; filename*0=UTF-8''unescaped\r\n"
-    + " filename*1*=%20so%20includes%20UTF-8''%20in%20value", 
-  "UTF-8''unescaped so includes UTF-8'' in value"],
-/* BROKEN: we prepend 'basic' to result
+   "extended", "extended"],
+
   // sneaky: if unescaped, make sure we leave UTF-8'' in value
-  ["Content-Disposition: attachment; filename=basic; filename*0=UTF-8''unescaped\r\n"
+  ["attachment; filename*0=UTF-8''unescaped\r\n"
     + " filename*1*=%20so%20includes%20UTF-8''%20in%20value", 
-  "UTF-8''unescaped so includes UTF-8'' in value"],
-*/
-/*  BROKEN: we append filename*1 to 'basic'
-  // Also not sure if this is the spec'd behavior here:
-  ["Content-Disposition: attachment; filename=basic; filename*1=multi\r\n"
+   "UTF-8''unescaped so includes UTF-8'' in value", Cr.NS_ERROR_INVALID_ARG],
+
+  // sneaky: if unescaped, make sure we leave UTF-8'' in value
+  ["attachment; filename=basic; filename*0=UTF-8''unescaped\r\n"
+    + " filename*1*=%20so%20includes%20UTF-8''%20in%20value", 
+   "UTF-8''unescaped so includes UTF-8'' in value", "basic"],
+
+  // Prefer basic over invalid continuation
+  ["attachment; filename=basic; filename*1=multi\r\n"
     + " filename*2=line\r\n" 
     + " filename*3*=%20extended",
-  "basic"],
-*/
+   "basic", "basic"],
+
+  // support digits over 10
+  ["attachment; filename=basic; filename*0*=UTF-8''0\r\n"
+    + " filename*1=1; filename*2=2;filename*3=3;filename*4=4;filename*5=5\r\n" 
+    + " filename*6=6; filename*7=7;filename*8=8;filename*9=9;filename*10=a\r\n"
+    + " filename*11=b; filename*12=c;filename*13=d;filename*14=e;filename*15=f\r\n",
+   "0123456789abcdef", "basic"],
+
+  // support digits over 10 (check ordering)
+  ["attachment; filename=basic; filename*0*=UTF-8''0\r\n"
+    + " filename*1=1; filename*2=2;filename*3=3;filename*4=4;filename*5=5\r\n" 
+    + " filename*6=6; filename*7=7;filename*8=8;filename*9=9;filename*10=a\r\n"
+    + " filename*11=b; filename*12=c;filename*13=d;filename*15=f;filename*14=e\r\n",
+   "0123456789abcd" /* should see the 'f', see bug 588414 */, "basic"],
+
+  // support digits over 10 (detect gaps)
+  ["attachment; filename=basic; filename*0*=UTF-8''0\r\n"
+    + " filename*1=1; filename*2=2;filename*3=3;filename*4=4;filename*5=5\r\n" 
+    + " filename*6=6; filename*7=7;filename*8=8;filename*9=9;filename*10=a\r\n"
+    + " filename*11=b; filename*12=c;filename*14=e\r\n",
+   "0123456789abc", "basic"],
+
+  // return nothing: invalid
+  ["attachment; filename*1=multi\r\n"
+    + " filename*2=line\r\n" 
+    + " filename*3*=%20extended",
+   Cr.NS_ERROR_INVALID_ARG, Cr.NS_ERROR_INVALID_ARG],
+
 ];
 
-var broken = [
-  ["Content-Disposition: attachment; filename*1=multi\r\n"
-    + " filename*2=line\r\n" 
-    + " filename*3*=%20extended",
-  "param continuation must start from 0: should fail"],
-];
-
-
-function run_test() {
-
+function do_tests(whichRFC)
+{
   var mhp = Components.classes["@mozilla.org/network/mime-hdrparam;1"]
                       .getService(Components.interfaces.nsIMIMEHeaderParam);
 
   var unused = { value : null };
 
-  for (var i = 0; i < succeed.length; ++i) {
-    dump("Testing " + succeed[i] + "\n");
+  for (var i = 0; i < tests.length; ++i) {
+    dump("Testing " + tests[i] + "\n");
     try {
-      do_check_eq(mhp.getParameter(succeed[i][0], "filename", "UTF-8", true, unused),
-                  succeed[i][1]);
-    } catch (e) {}
-  }
-
-  // Check failure cases
-  for (var i = 0; i < broken.length; ++i) {
-    dump("Testing " + broken[i] + "\n");
-    try {
-      var result = mhp.getParameter(broken[i][0], "filename", "UTF-8", true, unused);
-      // No exception?  Error. 
-      do_check_eq(broken[i][1], "instead got: " + result);
-    } catch (e) {
-      // .result set if getParameter failed: check for correct error code
-      if (e.result)
-        do_check_eq(e.result, Components.results.NS_ERROR_OUT_OF_MEMORY);
+      var result;
+      if (whichRFC == 1)
+        result = mhp.getParameter(tests[i][0], "filename", "UTF-8", true, unused);
+      else 
+        result = mhp.getParameter5987(tests[i][0], "filename", "UTF-8", true, unused);
+      do_check_eq(result, tests[i][whichRFC]);
+    } 
+    catch (e) {
+      // Tests can also succeed by expecting to fail with given error code
+      if (e.result) {
+        // Allow following tests to run by catching exception from do_check_eq()
+        try { 
+          do_check_eq(e.result, tests[i][whichRFC]); 
+        } catch(e) {}  
+      }
+      continue;
     }
   }
 }
 
+function run_test() {
+
+  // Test RFC 2231
+  do_tests(1);
+
+  // Test RFC 5987
+  do_tests(2);
+}
+