Repository: solid/community-server
Pull request #13 (2020-07-06 10:17:44)

feat: Fully support Accept* headers
I was unable to find a decent library that parsed accept headers and returned their weights (specifically the last part was hard to find) so I just wrote my own. Only thing I wondered about was whether this maybe should be a separate library (and if yes in my personal github or in the solid one).

joachimvh: I was already looking at it and it actually was quite easy to do since everything is split in separate little pieces so I added validation as well. Not 100% sure all syntax errors get found but should be most of them at least.

Patch:
diff --git a/src/ldp/http/AcceptPreferenceParser.ts b/src/ldp/http/AcceptPreferenceParser.ts
new file mode 100644
index 000000000..9d1f9ef0c
--- /dev/null
+++ b/src/ldp/http/AcceptPreferenceParser.ts
@@ -0,0 +1,62 @@
+import { HttpRequest } from '../../server/HttpRequest';
+import { PreferenceParser } from './PreferenceParser';
+import { RepresentationPreference } from '../representation/RepresentationPreference';
+import { RepresentationPreferences } from '../representation/RepresentationPreferences';
+import {
+  AcceptHeader,
+  parseAccept,
+  parseAcceptCharset,
+  parseAcceptEncoding,
+  parseAcceptLanguage,
+} from '../../util/AcceptParser';
+
+/**
+ * Extracts preferences from the accept-* headers from an incoming {@link HttpRequest}.
+ * Supports Accept, Accept-Charset, Accept-Encoding, Accept-Language and Accept-DateTime.
+ */
+export class AcceptPreferenceParser extends PreferenceParser {
+  public constructor() {
+    super();
+  }
+
+  public async canHandle(): Promise<void> {
+    return undefined;
+  }
+
+  public async handle(input: HttpRequest): Promise<RepresentationPreferences> {
+    const result: RepresentationPreferences = {};
+    const headers: { [T in keyof RepresentationPreferences]: { val: string; func: (input: string) => AcceptHeader[] }} = {
+      type: { val: input.headers.accept, func: parseAccept },
+      charset: { val: input.headers['accept-charset'] as string, func: parseAcceptCharset },
+      encoding: { val: input.headers['accept-encoding'] as string, func: parseAcceptEncoding },
+      language: { val: input.headers['accept-language'], func: parseAcceptLanguage },
+    };
+    Object.keys(headers).forEach((key: keyof RepresentationPreferences): void => {
+      const preferences = this.parseHeader(headers[key].val, headers[key].func);
+      if (preferences.length > 0) {
+        result[key] = preferences;
+      }
+    });
+
+    // Accept-DateTime is currently specified to simply have a datetime as value
+    if (input.headers['accept-datetime']) {
+      result.datetime = [{ value: input.headers['accept-datetime'] as string, weight: 1 }];
+    }
+
+    return result;
+  }
+
+  /**
+   * Converts a header string using the given parse function to {@link RepresentationPreference}[].
+   * @param input - Input header string.
+   * @param parseFunction - Function that converts header string to {@link AcceptHeader}.
+   *
+   * @returns A list of {@link RepresentationPreference}. Returns an empty list if input was not defined.
+   */
+  private parseHeader(input: string, parseFunction: (input: string) => AcceptHeader[]): RepresentationPreference[] {
+    if (!input) {
+      return [];
+    }
+    return parseFunction(input).map((accept): RepresentationPreference => ({ value: accept.range, weight: accept.weight }));
+  }
+}
diff --git a/src/ldp/http/SimplePreferenceParser.ts b/src/ldp/http/SimplePreferenceParser.ts
deleted file mode 100644
index 0700994b8..000000000
--- a/src/ldp/http/SimplePreferenceParser.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import { HttpRequest } from '../../server/HttpRequest';
-import { PreferenceParser } from './PreferenceParser';
-import { RepresentationPreference } from '../representation/RepresentationPreference';
-import { RepresentationPreferences } from '../representation/RepresentationPreferences';
-
-/**
- * Extracts preferences from the accept-* headers from an incoming {@link HttpRequest}.
- * Parsing of header strings is done naively.
- */
-export class SimplePreferenceParser extends PreferenceParser {
-  public constructor() {
-    super();
-  }
-
-  public async canHandle(): Promise<void> {
-    return undefined;
-  }
-
-  public async handle(input: HttpRequest): Promise<RepresentationPreferences> {
-    const type = this.parseHeader(input.headers.accept);
-    const charset = this.parseHeader(input.headers['accept-charset'] as string);
-    const language = this.parseHeader(input.headers['accept-language']);
-
-    // Datetime can have commas so requires separate rules
-    let datetime;
-    if (input.headers['accept-datetime']) {
-      datetime = [{ value: input.headers['accept-datetime'] as string, weight: 1 }];
-    }
-
-    return { type, charset, datetime, language };
-  }
-
-  private parseHeader(header: string): RepresentationPreference[] {
-    if (!header) {
-      return undefined;
-    }
-
-    return header.split(',').map((preference): RepresentationPreference => {
-      const parts = preference.split(';');
-      if (parts.length === 1) {
-        return { value: parts[0].trim(), weight: 1 };
-      }
-      return { value: parts[0].trim(), weight: parseFloat(parts[1].trim().slice('q='.length)) };
-    });
-  }
-}
diff --git a/src/util/AcceptParser.ts b/src/util/AcceptParser.ts
new file mode 100644
index 000000000..d8e854f2c
--- /dev/null
+++ b/src/util/AcceptParser.ts
@@ -0,0 +1,304 @@
+import { UnsupportedHttpError } from './errors/UnsupportedHttpError';
+
+// BNF based on https://tools.ietf.org/html/rfc7231
+//
+// Accept =          #( media-range [ accept-params ] )
+// Accept-Charset =  1#( ( charset / "*" ) [ weight ] )
+// Accept-Encoding  = #( codings [ weight ] )
+// Accept-Language = 1#( language-range [ weight ] )
+//
+// Content-Type = media-type
+// media-type = type "/" subtype *( OWS ";" OWS parameter )
+//
+// media-range    = ( "*/*"
+//                / ( type "/" "*" )
+//                / ( type "/" subtype )
+//                ) *( OWS ";" OWS parameter ) ; media type parameters
+// accept-params  = weight *( accept-ext )
+// accept-ext     = OWS ";" OWS token [ "=" ( token / quoted-string ) ] ; extension parameters
+//
+// weight = OWS ";" OWS "q=" qvalue
+// qvalue = ( "0" [ "." 0*3DIGIT ] )
+//        / ( "1" [ "." 0*3("0") ] )
+//
+// type       = token
+// subtype    = token
+// parameter  = token "=" ( token / quoted-string )
+//
+// quoted-string  = DQUOTE *( qdtext / quoted-pair ) DQUOTE
+// qdtext         = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text
+// obs-text       = %x80-FF
+// quoted-pair    = "\" ( HTAB / SP / VCHAR / obs-text )
+//
+// charset = token
+//
+// codings          = content-coding / "identity" / "*"
+// content-coding   = token
+//
+// language-range   = (1*8ALPHA *("-" 1*8alphanum)) / "*"
+// alphanum         = ALPHA / DIGIT
+//
+// Delimiters are chosen from the set of US-ASCII visual characters not allowed in a token (DQUOTE and "(),/:;<=>?@[\]{}").
+// token          = 1*tchar
+// tchar          = "!" / "#" / "$" / "%" / "&" / "'" / "*"
+//                / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
+//                / DIGIT / ALPHA
+//                ; any VCHAR, except delimiters
+//
+
+// INTERFACES
+/**
+ * General interface for all Accept* headers.
+ */
+export interface AcceptHeader {
+  /** Requested range. Can be a specific value or `*`, matching all. */
+  range: string;
+  /** Weight of the preference [0, 1]. */
+  weight: number;
+}
+
+/**
+ * Contents of an HTTP Accept header.
+ * Range is type/subtype. Both can be `*`.
+ */
+export interface Accept extends AcceptHeader {
+  parameters: {
+    /** Media type parameters. These are the parameters that came before the q value. */
+    mediaType: { [key: string]: string };
+    /** Extension parameters. These are the parameters that came after the q value. Value will be an empty string if there was none. */
+    extension: { [key: string]: string };
+  };
+}
+
+/**
+ * Contents of an HTTP Accept-Charset header.
+ */
+export interface AcceptCharset extends AcceptHeader { }
+
+/**
+ * Contents of an HTTP Accept-Encoding header.
+ */
+export interface AcceptEncoding extends AcceptHeader { }
+
+/**
+ * Contents of an HTTP Accept-Language header.
+ */
+export interface AcceptLanguage extends AcceptHeader { }
+
+// REUSED REGEXES
+const token = /^[a-zA-Z0-9!#$%&'*+-.^_`|~]+$/u;
+
+// HELPER FUNCTIONS
+/**
+ * Replaces all double quoted strings in the input string with `"0"`, `"1"`, etc.
+ * @param input - The Accept header string.
+ *
+ * @returns The transformed string and a map with keys `"0"`, etc. and values the original string that was there.
+ */
+const transformQuotedStrings = (input: string): { result: string; replacements: { [id: string]: string } } => {
+  let idx = 0;
+  const replacements: { [id: string]: string } = {};
+  const result = input.replace(/"(?:[^"\\]|\\.)*"/gu, (match): string => {
+    // Not all characters allowed in quoted strings, see BNF above
+    if (!/^"(?:[\t !\u0023-\u005b\u005d-\u007e\u0080-\u00ff]|(?:\\[\t\u0020-\u007e\u0080-\u00ff]))*"$/u.test(match)) {
+      throw new UnsupportedHttpError(`Invalid quoted string in Accept header: ${match}. Check which characters are allowed`);
+    }
+    const replacement = `"${idx}"`;
+    replacements[replacement] = match;
+    idx += 1;
+    return replacement;
+  });
+  return { result, replacements };
+};
+
+/**
+ * Splits the input string on commas, trims all parts and filters out empty ones.
+ *
+ * @param input - Input header string.
+ */
+const splitAndClean = (input: string): string[] =>
+  input.split(',')
+    .map((part): string => part.trim())
+    .filter((part): boolean => part.length > 0);
+
+/**
+ * Checks if the input string matches the qvalue regex.
+ *
+ * @param qvalue - Input qvalue string (so "q=....").
+ *
+ * @throws {@link UnsupportedHttpError}
+ * Thrown on invalid syntax.
+ */
+const testQValue = (qvalue: string): void => {
+  if (!/^q=(?:(?:0(?:\.\d{0,3})?)|(?:1(?:\.0{0,3})?))$/u.test(qvalue)) {
+    throw new UnsupportedHttpError(`Invalid q value: ${qvalue} does not match ("q=" ( "0" [ "." 0*3DIGIT ] ) / ( "1" [ "." 0*3("0") ] )).`);
+  }
+};
+
+/**
+ * Parses a single media range with corresponding parameters from an Accept header.
+ * For every parameter value that is a double quoted string,
+ * we check if it is a key in the replacements map.
+ * If yes the value from the map gets inserted instead.
+ *
+ * @param part - A string corresponding to a media range and its corresponding parameters.
+ * @param replacements - The double quoted strings that need to be replaced.
+ *
+ * @throws {@link UnsupportedHttpError}
+ * Thrown on invalid type, qvalue or parameter syntax.
+ *
+ * @returns {@link Accept} object corresponding to the header string.
+ */
+const parseAcceptPart = (part: string, replacements: { [id: string]: string }): Accept => {
+  const [ range, ...parameters ] = part.split(';').map((param): string => param.trim());
+
+  // No reason to test differently for * since we don't check if the type exists
+  const [ type, subtype ] = range.split('/');
+  if (!type || !subtype || !token.test(type) || !token.test(subtype)) {
+    throw new Error(`Invalid Accept range: ${range} does not match ( "*/*" / ( token "/" "*" ) / ( token "/" token ) )`);
+  }
+
+  let weight = 1;
+  const mediaTypeParams: { [key: string]: string } = {};
+  const extensionParams: { [key: string]: string } = {};
+  let map = mediaTypeParams;
+  parameters.forEach((param): void => {
+    const [ name, value ] = param.split('=');
+
+    if (name === 'q') {
+      // Extension parameters appear after the q value
+      map = extensionParams;
+      testQValue(param);
+      weight = parseFloat(value);
+    } else {
+      // Test replaced string for easier check
+      // parameter  = token "=" ( token / quoted-string )
+      // second part is optional for extension parameters
+      if (!token.test(name) || !((map === extensionParams && !value) || (value && (/^"\d+"$/u.test(value) || token.test(value))))) {
+        throw new UnsupportedHttpError(`Invalid Accept parameter: ${param} does not match (token "=" ( token / quoted-string )). ` +
+        'Second part optional for extension parameters.');
+      }
+
+      let actualValue = value;
+      if (value && value.length > 0 && value.startsWith('"') && replacements[value]) {
+        actualValue = replacements[value];
+      }
+
+      // Value is optional for extension parameters
+      map[name] = actualValue || '';
+    }
+  });
+
+  return {
+    range,
+    weight,
+    parameters: {
+      mediaType: mediaTypeParams,
+      extension: extensionParams,
+    },
+  };
+};
+
+/**
+ * Parses an Accept-* header where each part is only a value and a weight, so roughly /.*(q=.*)?/ separated by commas.
+ * @param input - Input header string.
+ *
+ * @throws {@link UnsupportedHttpError}
+ * Thrown on invalid qvalue syntax.
+ *
+ * @returns An array of ranges and weights.
+ */
+const parseNoParameters = (input: string): { range: string; weight: number }[] => {
+  const parts = splitAndClean(input);
+
+  return parts.map((part): { range: string; weight: number } => {
+    const [ range, qvalue ] = part.split(';').map((param): string => param.trim());
+    const result = { range, weight: 1 };
+    if (qvalue) {
+      testQValue(qvalue);
+      result.weight = parseFloat(qvalue.split('=')[1]);
+    }
+    return result;
+  }).sort((left, right): number => right.weight - left.weight);
+};
+
+// EXPORTED FUNCTIONS
+
+/**
+ * Parses an Accept header string.
+ *
+ * @param input - The Accept header string.
+ *
+ * @throws {@link UnsupportedHttpError}
+ * Thrown on invalid header syntax.
+ *
+ * @returns An array of {@link Accept} objects, sorted by weight.
+ */
+export const parseAccept = (input: string): Accept[] => {
+  // Quoted strings could prevent split from having correct results
+  const { result, replacements } = transformQuotedStrings(input);
+  return splitAndClean(result)
+    .map((part): Accept => parseAcceptPart(part, replacements))
+    .sort((left, right): number => right.weight - left.weight);
+};
+
+/**
+ * Parses an Accept-Charset header string.
+ *
+ * @param input - The Accept-Charset header string.
+ *
+ * @throws {@link UnsupportedHttpError}
+ * Thrown on invalid header syntax.
+ *
+ * @returns An array of {@link AcceptCharset} objects, sorted by weight.
+ */
+export const parseAcceptCharset = (input: string): AcceptCharset[] => {
+  const results = parseNoParameters(input);
+  results.forEach((result): void => {
+    if (!token.test(result.range)) {
+      throw new UnsupportedHttpError(`Invalid Accept-Charset range: ${result.range} does not match (content-coding / "identity" / "*")`);
+    }
+  });
+  return results;
+};
+
+/**
+ * Parses an Accept-Encoding header string.
+ *
+ * @param input - The Accept-Encoding header string.
+ *
+ * @throws {@link UnsupportedHttpError}
+ * Thrown on invalid header syntax.
+ *
+ * @returns An array of {@link AcceptEncoding} objects, sorted by weight.
+ */
+export const parseAcceptEncoding = (input: string): AcceptEncoding[] => {
+  const results = parseNoParameters(input);
+  results.forEach((result): void => {
+    if (!token.test(result.range)) {
+      throw new UnsupportedHttpError(`Invalid Accept-Encoding range: ${result.range} does not match (charset / "*")`);
+    }
+  });
+  return results;
+};
+
+/**
+ * Parses an Accept-Language header string.
+ *
+ * @param input - The Accept-Language header string.
+ *
+ * @throws {@link UnsupportedHttpError}
+ * Thrown on invalid header syntax.
+ *
+ * @returns An array of {@link AcceptLanguage} objects, sorted by weight.
+ */
+export const parseAcceptLanguage = (input: string): AcceptLanguage[] => {
+  const results = parseNoParameters(input);
+  results.forEach((result): void => {
+    // (1*8ALPHA *("-" 1*8alphanum)) / "*"
+    if (result.range !== '*' && !/^[a-zA-Z]{1,8}(?:-[a-zA-Z0-9]{1,8})*$/u.test(result.range)) {
+      throw new UnsupportedHttpError(`Invalid Accept-Language range: ${result.range} does not match ((1*8ALPHA *("-" 1*8alphanum)) / "*")`);
+    }
+  });
+  return results;
+};


Test patch:
diff --git a/test/integration/AuthenticatedLdpHandler.test.ts b/test/integration/AuthenticatedLdpHandler.test.ts
index c87169b3f..25069ac72 100644
--- a/test/integration/AuthenticatedLdpHandler.test.ts
+++ b/test/integration/AuthenticatedLdpHandler.test.ts
@@ -1,3 +1,4 @@
+import { AcceptPreferenceParser } from '../../src/ldp/http/AcceptPreferenceParser';
 import { AuthenticatedLdpHandler } from '../../src/ldp/AuthenticatedLdpHandler';
 import { CompositeAsyncHandler } from '../../src/util/CompositeAsyncHandler';
 import { EventEmitter } from 'events';
@@ -11,7 +12,6 @@ import { SimpleDeleteOperationHandler } from '../../src/ldp/operations/SimpleDel
 import { SimpleGetOperationHandler } from '../../src/ldp/operations/SimpleGetOperationHandler';
 import { SimplePermissionsExtractor } from '../../src/ldp/permissions/SimplePermissionsExtractor';
 import { SimplePostOperationHandler } from '../../src/ldp/operations/SimplePostOperationHandler';
-import { SimplePreferenceParser } from '../../src/ldp/http/SimplePreferenceParser';
 import { SimpleRequestParser } from '../../src/ldp/http/SimpleRequestParser';
 import { SimpleResourceStore } from '../../src/storage/SimpleResourceStore';
 import { SimpleResponseWriter } from '../../src/ldp/http/SimpleResponseWriter';
@@ -25,7 +25,7 @@ describe('An AuthenticatedLdpHandler with instantiated handlers', (): void => {
   beforeEach(async(): Promise<void> => {
     const requestParser = new SimpleRequestParser({
       targetExtractor: new SimpleTargetExtractor(),
-      preferenceParser: new SimplePreferenceParser(),
+      preferenceParser: new AcceptPreferenceParser(),
       bodyParser: new SimpleBodyParser(),
     });
 
diff --git a/test/integration/RequestParser.test.ts b/test/integration/RequestParser.test.ts
index a2973b4b7..d631b511f 100644
--- a/test/integration/RequestParser.test.ts
+++ b/test/integration/RequestParser.test.ts
@@ -1,7 +1,7 @@
+import { AcceptPreferenceParser } from '../../src/ldp/http/AcceptPreferenceParser';
 import arrayifyStream from 'arrayify-stream';
 import { HttpRequest } from '../../src/server/HttpRequest';
 import { SimpleBodyParser } from '../../src/ldp/http/SimpleBodyParser';
-import { SimplePreferenceParser } from '../../src/ldp/http/SimplePreferenceParser';
 import { SimpleRequestParser } from '../../src/ldp/http/SimpleRequestParser';
 import { SimpleTargetExtractor } from '../../src/ldp/http/SimpleTargetExtractor';
 import streamifyArray from 'streamify-array';
@@ -11,7 +11,7 @@ import { namedNode, triple } from '@rdfjs/data-model';
 describe('A SimpleRequestParser with simple input parsers', (): void => {
   const targetExtractor = new SimpleTargetExtractor();
   const bodyParser = new SimpleBodyParser();
-  const preferenceParser = new SimplePreferenceParser();
+  const preferenceParser = new AcceptPreferenceParser();
   const requestParser = new SimpleRequestParser({ targetExtractor, bodyParser, preferenceParser });
 
   it('can parse an incoming request.', async(): Promise<void> => {
diff --git a/test/unit/ldp/http/SimplePreferenceParser.test.ts b/test/unit/ldp/http/AcceptPreferenceParser.test.ts
similarity index 70%
rename from test/unit/ldp/http/SimplePreferenceParser.test.ts
rename to test/unit/ldp/http/AcceptPreferenceParser.test.ts
index f4fd48e20..5189c662f 100644
--- a/test/unit/ldp/http/SimplePreferenceParser.test.ts
+++ b/test/unit/ldp/http/AcceptPreferenceParser.test.ts
@@ -1,8 +1,8 @@
+import { AcceptPreferenceParser } from '../../../../src/ldp/http/AcceptPreferenceParser';
 import { HttpRequest } from '../../../../src/server/HttpRequest';
-import { SimplePreferenceParser } from '../../../../src/ldp/http/SimplePreferenceParser';
 
-describe('A SimplePreferenceParser', (): void => {
-  const preferenceParser = new SimplePreferenceParser();
+describe('An AcceptPreferenceParser', (): void => {
+  const preferenceParser = new AcceptPreferenceParser();
 
   it('can handle all input.', async(): Promise<void> => {
     await expect(preferenceParser.canHandle()).resolves.toBeUndefined();
@@ -14,7 +14,7 @@ describe('A SimplePreferenceParser', (): void => {
 
   it('parses accept headers.', async(): Promise<void> => {
     await expect(preferenceParser.handle({ headers: { accept: 'audio/*; q=0.2, audio/basic' }} as HttpRequest))
-      .resolves.toEqual({ type: [{ value: 'audio/*', weight: 0.2 }, { value: 'audio/basic', weight: 1 }]});
+      .resolves.toEqual({ type: [{ value: 'audio/basic', weight: 1 }, { value: 'audio/*', weight: 0.2 }]});
   });
 
   it('parses accept-charset headers.', async(): Promise<void> => {
@@ -27,6 +27,11 @@ describe('A SimplePreferenceParser', (): void => {
       .resolves.toEqual({ datetime: [{ value: 'Tue, 20 Mar 2001 20:35:00 GMT', weight: 1 }]});
   });
 
+  it('parses accept-encoding headers.', async(): Promise<void> => {
+    await expect(preferenceParser.handle({ headers: { 'accept-encoding': 'gzip;q=1.0, identity; q=0.5, *;q=0' }} as unknown as HttpRequest))
+      .resolves.toEqual({ encoding: [{ value: 'gzip', weight: 1 }, { value: 'identity', weight: 0.5 }, { value: '*', weight: 0 }]});
+  });
+
   it('parses accept-language headers.', async(): Promise<void> => {
     await expect(preferenceParser.handle({ headers: { 'accept-language': 'da, en-gb;q=0.8, en;q=0.7' }} as HttpRequest))
       .resolves.toEqual({ language: [{ value: 'da', weight: 1 }, { value: 'en-gb', weight: 0.8 }, { value: 'en', weight: 0.7 }]});
diff --git a/test/unit/util/AcceptParser.test.ts b/test/unit/util/AcceptParser.test.ts
new file mode 100644
index 000000000..25a6c1d67
--- /dev/null
+++ b/test/unit/util/AcceptParser.test.ts
@@ -0,0 +1,123 @@
+import {
+  parseAccept,
+  parseAcceptCharset,
+  parseAcceptEncoding,
+  parseAcceptLanguage,
+} from '../../../src/util/AcceptParser';
+
+describe('AcceptParser', (): void => {
+  describe('parseAccept function', (): void => {
+    it('parses empty Accept headers.', async(): Promise<void> => {
+      expect(parseAccept('')).toEqual([]);
+    });
+
+    it('parses Accept headers with a single entry.', async(): Promise<void> => {
+      expect(parseAccept('audio/basic')).toEqual([
+        { range: 'audio/basic', weight: 1, parameters: { mediaType: {}, extension: {}}},
+      ]);
+    });
+
+    it('parses Accept headers with multiple entries.', async(): Promise<void> => {
+      expect(parseAccept('audio/*; q=0.2, audio/basic')).toEqual([
+        { range: 'audio/basic', weight: 1, parameters: { mediaType: {}, extension: {}}},
+        { range: 'audio/*', weight: 0.2, parameters: { mediaType: {}, extension: {}}},
+      ]);
+    });
+
+    it('parses complex Accept headers.', async(): Promise<void> => {
+      expect(parseAccept('text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4,text/x-dvi; q=0.8; mxb=100000; mxt')).toEqual([
+        { range: 'text/html', weight: 1, parameters: { mediaType: { level: '1' }, extension: {}}},
+        { range: 'text/x-dvi', weight: 0.8, parameters: { mediaType: {}, extension: { mxb: '100000', mxt: '' }}},
+        { range: 'text/html', weight: 0.7, parameters: { mediaType: {}, extension: {}}},
+        { range: 'text/html', weight: 0.4, parameters: { mediaType: { level: '2' }, extension: {}}},
+      ]);
+    });
+
+    it('parses Accept headers with double quoted values.', async(): Promise<void> => {
+      expect(parseAccept('audio/basic; param1="val" ; q=0.5 ;param2="\\\\\\"valid"')).toEqual([
+        { range: 'audio/basic', weight: 0.5, parameters: { mediaType: { param1: '"val"' }, extension: { param2: '"\\\\\\"valid"' }}},
+      ]);
+    });
+
+    it('rejects Accept Headers with invalid types.', async(): Promise<void> => {
+      expect((): any => parseAccept('*')).toThrow('Invalid Accept range:');
+      expect((): any => parseAccept('"bad"/text')).toThrow('Invalid Accept range:');
+      expect((): any => parseAccept('*/\\bad')).toThrow('Invalid Accept range:');
+      expect((): any => parseAccept('*/*')).not.toThrow('Invalid Accept range:');
+    });
+
+    it('rejects Accept Headers with invalid q values.', async(): Promise<void> => {
+      expect((): any => parseAccept('a/b; q=text')).toThrow('Invalid q value:');
+      expect((): any => parseAccept('a/b; q=0.1234')).toThrow('Invalid q value:');
+      expect((): any => parseAccept('a/b; q=1.1')).toThrow('Invalid q value:');
+      expect((): any => parseAccept('a/b; q=1.000')).not.toThrow();
+      expect((): any => parseAccept('a/b; q=0.123')).not.toThrow();
+    });
+
+    it('rejects Accept Headers with invalid parameters.', async(): Promise<void> => {
+      expect((): any => parseAccept('a/b; a')).toThrow('Invalid Accept parameter:');
+      expect((): any => parseAccept('a/b; a=\\')).toThrow('Invalid Accept parameter:');
+      expect((): any => parseAccept('a/b; q=1 ; a=\\')).toThrow('Invalid Accept parameter:');
+      expect((): any => parseAccept('a/b; q=1 ; a')).not.toThrow('Invalid Accept parameter:');
+    });
+
+    it('rejects Accept Headers with quoted parameters.', async(): Promise<void> => {
+      expect((): any => parseAccept('a/b; a="\\""')).not.toThrow();
+      expect((): any => parseAccept('a/b; a="\\\u007F"')).toThrow('Invalid quoted string in Accept header:');
+    });
+  });
+
+  describe('parseCharset function', (): void => {
+    it('parses Accept-Charset headers.', async(): Promise<void> => {
+      expect(parseAcceptCharset('iso-8859-5, unicode-1-1;q=0.8')).toEqual([
+        { range: 'iso-8859-5', weight: 1 },
+        { range: 'unicode-1-1', weight: 0.8 },
+      ]);
+    });
+
+    it('rejects invalid Accept-Charset Headers.', async(): Promise<void> => {
+      expect((): any => parseAcceptCharset('a/b')).toThrow('Invalid Accept-Charset range:');
+      expect((): any => parseAcceptCharset('a; q=text')).toThrow('Invalid q value:');
+    });
+  });
+
+  describe('parseEncoding function', (): void => {
+    it('parses empty Accept-Encoding headers.', async(): Promise<void> => {
+      expect(parseAcceptCharset('')).toEqual([]);
+    });
+
+    it('parses Accept-Encoding headers.', async(): Promise<void> => {
+      expect(parseAcceptEncoding('gzip;q=1.000, identity; q=0.5, *;q=0')).toEqual([
+        { range: 'gzip', weight: 1 },
+        { range: 'identity', weight: 0.5 },
+        { range: '*', weight: 0 },
+      ]);
+    });
+
+    it('rejects invalid Accept-Encoding Headers.', async(): Promise<void> => {
+      expect((): any => parseAcceptEncoding('a/b')).toThrow('Invalid Accept-Encoding range:');
+      expect((): any => parseAcceptEncoding('a; q=text')).toThrow('Invalid q value:');
+    });
+  });
+
+  describe('parseLanguage function', (): void => {
+    it('parses Accept-Language headers.', async(): Promise<void> => {
+      expect(parseAcceptLanguage('da, en-gb;q=0.8, en;q=0.7')).toEqual([
+        { range: 'da', weight: 1 },
+        { range: 'en-gb', weight: 0.8 },
+        { range: 'en', weight: 0.7 },
+      ]);
+    });
+
+    it('rejects invalid Accept-Language Headers.', async(): Promise<void> => {
+      expect((): any => parseAcceptLanguage('a/b')).toThrow('Invalid Accept-Language range:');
+      expect((): any => parseAcceptLanguage('05-a')).toThrow('Invalid Accept-Language range:');
+      expect((): any => parseAcceptLanguage('a--05')).toThrow('Invalid Accept-Language range:');
+      expect((): any => parseAcceptLanguage('a-"a"')).toThrow('Invalid Accept-Language range:');
+      expect((): any => parseAcceptLanguage('a-05')).not.toThrow('Invalid Accept-Language range:');
+      expect((): any => parseAcceptLanguage('a-b-c-d')).not.toThrow('Invalid Accept-Language range:');
+
+      expect((): any => parseAcceptLanguage('a; q=text')).toThrow('Invalid q value:');
+    });
+  });
+});