From 8fcdbb003cbee075aa09bd2ff3c9ea74a40f6525 Mon Sep 17 00:00:00 2001 From: Jonatan Nilsson Date: Mon, 4 Jan 2021 14:06:20 +0000 Subject: [PATCH] remove more dependencies --- lib/content-disposition.js | 456 ++++++++++++++++++ lib/fresh.js | 137 ++++++ lib/request.js | 2 +- lib/response.js | 2 +- package.json | 2 - test/request/fresh.js | 187 +++++++ test/response/content.js | 962 +++++++++++++++++++++++++++++++++++++ 7 files changed, 1744 insertions(+), 4 deletions(-) create mode 100644 lib/content-disposition.js create mode 100644 lib/fresh.js create mode 100644 test/response/content.js diff --git a/lib/content-disposition.js b/lib/content-disposition.js new file mode 100644 index 0000000..3323d02 --- /dev/null +++ b/lib/content-disposition.js @@ -0,0 +1,456 @@ +/*! + * content-disposition + * Copyright(c) 2014-2017 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict' + +/** + * Module exports. + * @public + */ + +module.exports = contentDisposition +module.exports.parse = parse + +/** + * Module dependencies. + * @private + */ + +const { basename } = require('path') + +/** + * RegExp to match non attr-char, *after* encodeURIComponent (i.e. not including "%") + * @private + */ + +const ENCODE_URL_ATTR_CHAR_REGEXP = /[\x00-\x20"'()*,/:;<=>?@[\\\]{}\x7f]/g // eslint-disable-line no-control-regex + +/** + * RegExp to match percent encoding escape. + * @private + */ + +const HEX_ESCAPE_REGEXP = /%[0-9A-Fa-f]{2}/ +const HEX_ESCAPE_REPLACE_REGEXP = /%([0-9A-Fa-f]{2})/g + +/** + * RegExp to match non-latin1 characters. + * @private + */ + +const NON_LATIN1_REGEXP = /[^\x20-\x7e\xa0-\xff]/g + +/** + * RegExp to match quoted-pair in RFC 2616 + * + * quoted-pair = "\" CHAR + * CHAR = + * @private + */ + +const QESC_REGEXP = /\\([\u0000-\u007f])/g // eslint-disable-line no-control-regex + +/** + * RegExp to match chars that must be quoted-pair in RFC 2616 + * @private + */ + +const QUOTE_REGEXP = /([\\"])/g + +/** + * RegExp for various RFC 2616 grammar + * + * parameter = token "=" ( token | quoted-string ) + * token = 1* + * separators = "(" | ")" | "<" | ">" | "@" + * | "," | ";" | ":" | "\" | <"> + * | "/" | "[" | "]" | "?" | "=" + * | "{" | "}" | SP | HT + * quoted-string = ( <"> *(qdtext | quoted-pair ) <"> ) + * qdtext = > + * quoted-pair = "\" CHAR + * CHAR = + * TEXT = + * LWS = [CRLF] 1*( SP | HT ) + * CRLF = CR LF + * CR = + * LF = + * SP = + * HT = + * CTL = + * OCTET = + * @private + */ + +const PARAM_REGEXP = /;[\x09\x20]*([!#$%&'*+.0-9A-Z^_`a-z|~-]+)[\x09\x20]*=[\x09\x20]*("(?:[\x20!\x23-\x5b\x5d-\x7e\x80-\xff]|\\[\x20-\x7e])*"|[!#$%&'*+.0-9A-Z^_`a-z|~-]+)[\x09\x20]*/g // eslint-disable-line no-control-regex +const TEXT_REGEXP = /^[\x20-\x7e\x80-\xff]+$/ +const TOKEN_REGEXP = /^[!#$%&'*+.0-9A-Z^_`a-z|~-]+$/ + +/** + * RegExp for various RFC 5987 grammar + * + * ext-value = charset "'" [ language ] "'" value-chars + * charset = "UTF-8" / "ISO-8859-1" / mime-charset + * mime-charset = 1*mime-charsetc + * mime-charsetc = ALPHA / DIGIT + * / "!" / "#" / "$" / "%" / "&" + * / "+" / "-" / "^" / "_" / "`" + * / "{" / "}" / "~" + * language = ( 2*3ALPHA [ extlang ] ) + * / 4ALPHA + * / 5*8ALPHA + * extlang = *3( "-" 3ALPHA ) + * value-chars = *( pct-encoded / attr-char ) + * pct-encoded = "%" HEXDIG HEXDIG + * attr-char = ALPHA / DIGIT + * / "!" / "#" / "$" / "&" / "+" / "-" / "." + * / "^" / "_" / "`" / "|" / "~" + * @private + */ + +const EXT_VALUE_REGEXP = /^([A-Za-z0-9!#$%&+\-^_`{}~]+)'(?:[A-Za-z]{2,3}(?:-[A-Za-z]{3}){0,3}|[A-Za-z]{4,8}|)'((?:%[0-9A-Fa-f]{2}|[A-Za-z0-9!#$&+.^_`|~-])+)$/ + +/** + * RegExp for various RFC 6266 grammar + * + * disposition-type = "inline" | "attachment" | disp-ext-type + * disp-ext-type = token + * disposition-parm = filename-parm | disp-ext-parm + * filename-parm = "filename" "=" value + * | "filename*" "=" ext-value + * disp-ext-parm = token "=" value + * | ext-token "=" ext-value + * ext-token = + * @private + */ + +const DISPOSITION_TYPE_REGEXP = /^([!#$%&'*+.0-9A-Z^_`a-z|~-]+)[\x09\x20]*(?:$|;)/ // eslint-disable-line no-control-regex + +/** + * Create an attachment Content-Disposition header. + * + * @param {string} [filename] + * @param {object} [options] + * @param {string} [options.type=attachment] + * @param {string|boolean} [options.fallback=true] + * @return {string} + * @public + */ + +function contentDisposition (filename, options) { + const opts = options || {} + + // get type + const type = opts.type || 'attachment' + + // get parameters + const params = createparams(filename, opts.fallback) + + // format into string + return format(new ContentDisposition(type, params)) +} + +/** + * Create parameters object from filename and fallback. + * + * @param {string} [filename] + * @param {string|boolean} [fallback=true] + * @return {object} + * @private + */ + +function createparams (filename, fallback) { + if (filename === undefined) { + return + } + + const params = {} + + if (typeof filename !== 'string') { + throw new TypeError('filename must be a string') + } + + // fallback defaults to true + if (fallback === undefined) { + fallback = true + } + + if (typeof fallback !== 'string' && typeof fallback !== 'boolean') { + throw new TypeError('fallback must be a string or boolean') + } + + if (typeof fallback === 'string' && NON_LATIN1_REGEXP.test(fallback)) { + throw new TypeError('fallback must be ISO-8859-1 string') + } + + // restrict to file base name + const name = basename(filename) + + // determine if name is suitable for quoted string + const isQuotedString = TEXT_REGEXP.test(name) + + // generate fallback name + const fallbackName = typeof fallback !== 'string' + ? fallback && getlatin1(name) + : basename(fallback) + const hasFallback = typeof fallbackName === 'string' && fallbackName !== name + + // set extended filename parameter + if (hasFallback || !isQuotedString || HEX_ESCAPE_REGEXP.test(name)) { + params['filename*'] = name + } + + // set filename parameter + if (isQuotedString || hasFallback) { + params.filename = hasFallback + ? fallbackName + : name + } + + return params +} + +/** + * Format object to Content-Disposition header. + * + * @param {object} obj + * @param {string} obj.type + * @param {object} [obj.parameters] + * @return {string} + * @private + */ + +function format (obj) { + const parameters = obj.parameters + const type = obj.type + + if (!type || typeof type !== 'string' || !TOKEN_REGEXP.test(type)) { + throw new TypeError('invalid type') + } + + // start with normalized type + let string = String(type).toLowerCase() + + // append parameters + if (parameters && typeof parameters === 'object') { + const params = Object.keys(parameters).sort() + + for (let i = 0; i < params.length; i++) { + const param = params[i] + + const val = param.substr(-1) === '*' + ? ustring(parameters[param]) + : qstring(parameters[param]) + + string += '; ' + param + '=' + val + } + } + + return string +} + +/** + * Decode a RFC 6987 field value (gracefully). + * + * @param {string} str + * @return {string} + * @private + */ + +function decodefield (str) { + const match = EXT_VALUE_REGEXP.exec(str) + + if (!match) { + throw new TypeError('invalid extended field value') + } + + const charset = match[1].toLowerCase() + const encoded = match[2] + let value + + // to binary string + const binary = encoded.replace(HEX_ESCAPE_REPLACE_REGEXP, pdecode) + + switch (charset) { + case 'iso-8859-1': + value = getlatin1(binary) + break + case 'utf-8': + value = Buffer.from(binary, 'binary').toString('utf8') + break + default: + throw new TypeError('unsupported charset in extended field') + } + + return value +} + +/** + * Get ISO-8859-1 version of string. + * + * @param {string} val + * @return {string} + * @private + */ + +function getlatin1 (val) { + // simple Unicode -> ISO-8859-1 transformation + return String(val).replace(NON_LATIN1_REGEXP, '?') +} + +/** + * Parse Content-Disposition header string. + * + * @param {string} string + * @return {object} + * @public + */ + +function parse (string) { + if (!string || typeof string !== 'string') { + throw new TypeError('argument string is required') + } + + let match = DISPOSITION_TYPE_REGEXP.exec(string) + + if (!match) { + throw new TypeError('invalid type format') + } + + // normalize type + let index = match[0].length + const type = match[1].toLowerCase() + + let key + let value + const names = [] + const params = {} + + // calculate index to start at + index = PARAM_REGEXP.lastIndex = match[0].substr(-1) === ';' + ? index - 1 + : index + + // match parameters + while ((match = PARAM_REGEXP.exec(string))) { + if (match.index !== index) { + throw new TypeError('invalid parameter format') + } + + index += match[0].length + key = match[1].toLowerCase() + value = match[2] + + if (names.indexOf(key) !== -1) { + throw new TypeError('invalid duplicate parameter') + } + + names.push(key) + + if (key.indexOf('*') + 1 === key.length) { + // decode extended value + key = key.slice(0, -1) + value = decodefield(value) + + // overwrite existing value + params[key] = value + continue + } + + if (typeof params[key] === 'string') { + continue + } + + if (value[0] === '"') { + // remove quotes and escapes + value = value + .substr(1, value.length - 2) + .replace(QESC_REGEXP, '$1') + } + + params[key] = value + } + + if (index !== -1 && index !== string.length) { + throw new TypeError('invalid parameter format') + } + + return new ContentDisposition(type, params) +} + +/** + * Percent decode a single character. + * + * @param {string} str + * @param {string} hex + * @return {string} + * @private + */ + +function pdecode (str, hex) { + return String.fromCharCode(parseInt(hex, 16)) +} + +/** + * Percent encode a single character. + * + * @param {string} char + * @return {string} + * @private + */ + +function pencode (char) { + return '%' + String(char) + .charCodeAt(0) + .toString(16) + .toUpperCase() +} + +/** + * Quote a string for HTTP. + * + * @param {string} val + * @return {string} + * @private + */ + +function qstring (val) { + const str = String(val) + + return '"' + str.replace(QUOTE_REGEXP, '\\$1') + '"' +} + +/** + * Encode a Unicode string for HTTP (RFC 5987). + * + * @param {string} val + * @return {string} + * @private + */ + +function ustring (val) { + const str = String(val) + + // percent encode as UTF-8 + const encoded = encodeURIComponent(str) + .replace(ENCODE_URL_ATTR_CHAR_REGEXP, pencode) + + return 'UTF-8\'\'' + encoded +} + +/** + * Class for parsed Content-Disposition header for v8 optimization + * + * @public + * @param {string} type + * @param {object} parameters + * @constructor + */ + +function ContentDisposition (type, parameters) { + this.type = type + this.parameters = parameters +} diff --git a/lib/fresh.js b/lib/fresh.js new file mode 100644 index 0000000..d154f5a --- /dev/null +++ b/lib/fresh.js @@ -0,0 +1,137 @@ +/*! + * fresh + * Copyright(c) 2012 TJ Holowaychuk + * Copyright(c) 2016-2017 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict' + +/** + * RegExp to check for no-cache token in Cache-Control. + * @private + */ + +var CACHE_CONTROL_NO_CACHE_REGEXP = /(?:^|,)\s*?no-cache\s*?(?:,|$)/ + +/** + * Module exports. + * @public + */ + +module.exports = fresh + +/** + * Check freshness of the response using request and response headers. + * + * @param {Object} reqHeaders + * @param {Object} resHeaders + * @return {Boolean} + * @public + */ + +function fresh (reqHeaders, resHeaders) { + // fields + var modifiedSince = reqHeaders['if-modified-since'] + var noneMatch = reqHeaders['if-none-match'] + + // unconditional request + if (!modifiedSince && !noneMatch) { + return false + } + + // Always return stale when Cache-Control: no-cache + // to support end-to-end reload requests + // https://tools.ietf.org/html/rfc2616#section-14.9.4 + var cacheControl = reqHeaders['cache-control'] + if (cacheControl && CACHE_CONTROL_NO_CACHE_REGEXP.test(cacheControl)) { + return false + } + + // if-none-match + if (noneMatch && noneMatch !== '*') { + var etag = resHeaders['etag'] + + if (!etag) { + return false + } + + var etagStale = true + var matches = parseTokenList(noneMatch) + for (var i = 0; i < matches.length; i++) { + var match = matches[i] + if (match === etag || match === 'W/' + etag || 'W/' + match === etag) { + etagStale = false + break + } + } + + if (etagStale) { + return false + } + } + + // if-modified-since + if (modifiedSince) { + var lastModified = resHeaders['last-modified'] + var modifiedStale = !lastModified || !(parseHttpDate(lastModified) <= parseHttpDate(modifiedSince)) + + if (modifiedStale) { + return false + } + } + + return true +} + +/** + * Parse an HTTP Date into a number. + * + * @param {string} date + * @private + */ + +function parseHttpDate (date) { + var timestamp = date && Date.parse(date) + + // istanbul ignore next: guard against date.js Date.parse patching + return typeof timestamp === 'number' + ? timestamp + : NaN +} + +/** + * Parse a HTTP token list. + * + * @param {string} str + * @private + */ + +function parseTokenList (str) { + var end = 0 + var list = [] + var start = 0 + + // gather tokens + for (var i = 0, len = str.length; i < len; i++) { + switch (str.charCodeAt(i)) { + case 0x20: /* */ + if (start === end) { + start = end = i + 1 + } + break + case 0x2c: /* , */ + list.push(str.substring(start, end)) + start = end = i + 1 + break + default: + end = i + 1 + break + } + } + + // final token + list.push(str.substring(start, end)) + + return list +} diff --git a/lib/request.js b/lib/request.js index 9733892..2d16760 100644 --- a/lib/request.js +++ b/lib/request.js @@ -9,8 +9,8 @@ const URL = require('url').URL; const net = require('net'); const stringify = require('url').format; const qs = require('querystring'); -const fresh = require('fresh'); const util = require('util'); +const fresh = require('./fresh'); const fastparse = require('./fastparse'); const accepts = require('./accepts'); diff --git a/lib/response.js b/lib/response.js index f38bf98..15a8257 100644 --- a/lib/response.js +++ b/lib/response.js @@ -6,7 +6,7 @@ */ const ReadStream = require('fs').ReadStream; -const contentDisposition = require('content-disposition'); +const contentDisposition = require('./content-disposition'); const assert = require('assert'); const extname = require('path').extname; const util = require('util'); diff --git a/package.json b/package.json index 8cc4a30..5f7d96f 100644 --- a/package.json +++ b/package.json @@ -22,9 +22,7 @@ ], "license": "MIT", "dependencies": { - "content-disposition": "jharrilim/content-disposition#572383f", "debug-ms": "~4.1.2", - "fresh": "~0.5.2", "http-errors-lite": "^2.0.2" }, "devDependencies": { diff --git a/test/request/fresh.js b/test/request/fresh.js index 7a5363e..816b66f 100644 --- a/test/request/fresh.js +++ b/test/request/fresh.js @@ -3,6 +3,7 @@ const assert = require('assert'); const context = require('../helpers/context'); +const fresh = require('../../lib/fresh') describe('ctx.fresh', () => { describe('the request method is not GET and HEAD', () => { @@ -48,3 +49,189 @@ describe('ctx.fresh', () => { }); }); }); + +describe('fresh(reqHeaders, resHeaders)', function () { + describe('when a non-conditional GET is performed', function () { + it('should be stale', function () { + var reqHeaders = {} + var resHeaders = {} + assert.ok(!fresh(reqHeaders, resHeaders)) + }) + }) + + describe('when requested with If-None-Match', function () { + describe('when ETags match', function () { + it('should be fresh', function () { + var reqHeaders = { 'if-none-match': '"foo"' } + var resHeaders = { 'etag': '"foo"' } + assert.ok(fresh(reqHeaders, resHeaders)) + }) + }) + + describe('when ETags mismatch', function () { + it('should be stale', function () { + var reqHeaders = { 'if-none-match': '"foo"' } + var resHeaders = { 'etag': '"bar"' } + assert.ok(!fresh(reqHeaders, resHeaders)) + }) + }) + + describe('when at least one matches', function () { + it('should be fresh', function () { + var reqHeaders = { 'if-none-match': ' "bar" , "foo"' } + var resHeaders = { 'etag': '"foo"' } + assert.ok(fresh(reqHeaders, resHeaders)) + }) + }) + + describe('when etag is missing', function () { + it('should be stale', function () { + var reqHeaders = { 'if-none-match': '"foo"' } + var resHeaders = {} + assert.ok(!fresh(reqHeaders, resHeaders)) + }) + }) + + describe('when ETag is weak', function () { + it('should be fresh on exact match', function () { + var reqHeaders = { 'if-none-match': 'W/"foo"' } + var resHeaders = { 'etag': 'W/"foo"' } + assert.ok(fresh(reqHeaders, resHeaders)) + }) + + it('should be fresh on strong match', function () { + var reqHeaders = { 'if-none-match': 'W/"foo"' } + var resHeaders = { 'etag': '"foo"' } + assert.ok(fresh(reqHeaders, resHeaders)) + }) + }) + + describe('when ETag is strong', function () { + it('should be fresh on exact match', function () { + var reqHeaders = { 'if-none-match': '"foo"' } + var resHeaders = { 'etag': '"foo"' } + assert.ok(fresh(reqHeaders, resHeaders)) + }) + + it('should be fresh on weak match', function () { + var reqHeaders = { 'if-none-match': '"foo"' } + var resHeaders = { 'etag': 'W/"foo"' } + assert.ok(fresh(reqHeaders, resHeaders)) + }) + }) + + describe('when * is given', function () { + it('should be fresh', function () { + var reqHeaders = { 'if-none-match': '*' } + var resHeaders = { 'etag': '"foo"' } + assert.ok(fresh(reqHeaders, resHeaders)) + }) + + it('should get ignored if not only value', function () { + var reqHeaders = { 'if-none-match': '*, "bar"' } + var resHeaders = { 'etag': '"foo"' } + assert.ok(!fresh(reqHeaders, resHeaders)) + }) + }) + }) + + describe('when requested with If-Modified-Since', function () { + describe('when modified since the date', function () { + it('should be stale', function () { + var reqHeaders = { 'if-modified-since': 'Sat, 01 Jan 2000 00:00:00 GMT' } + var resHeaders = { 'last-modified': 'Sat, 01 Jan 2000 01:00:00 GMT' } + assert.ok(!fresh(reqHeaders, resHeaders)) + }) + }) + + describe('when unmodified since the date', function () { + it('should be fresh', function () { + var reqHeaders = { 'if-modified-since': 'Sat, 01 Jan 2000 01:00:00 GMT' } + var resHeaders = { 'last-modified': 'Sat, 01 Jan 2000 00:00:00 GMT' } + assert.ok(fresh(reqHeaders, resHeaders)) + }) + }) + + describe('when Last-Modified is missing', function () { + it('should be stale', function () { + var reqHeaders = { 'if-modified-since': 'Sat, 01 Jan 2000 00:00:00 GMT' } + var resHeaders = {} + assert.ok(!fresh(reqHeaders, resHeaders)) + }) + }) + + describe('with invalid If-Modified-Since date', function () { + it('should be stale', function () { + var reqHeaders = { 'if-modified-since': 'foo' } + var resHeaders = { 'last-modified': 'Sat, 01 Jan 2000 00:00:00 GMT' } + assert.ok(!fresh(reqHeaders, resHeaders)) + }) + }) + + describe('with invalid Last-Modified date', function () { + it('should be stale', function () { + var reqHeaders = { 'if-modified-since': 'Sat, 01 Jan 2000 00:00:00 GMT' } + var resHeaders = { 'last-modified': 'foo' } + assert.ok(!fresh(reqHeaders, resHeaders)) + }) + }) + }) + + describe('when requested with If-Modified-Since and If-None-Match', function () { + describe('when both match', function () { + it('should be fresh', function () { + var reqHeaders = { 'if-none-match': '"foo"', 'if-modified-since': 'Sat, 01 Jan 2000 01:00:00 GMT' } + var resHeaders = { 'etag': '"foo"', 'last-modified': 'Sat, 01 Jan 2000 00:00:00 GMT' } + assert.ok(fresh(reqHeaders, resHeaders)) + }) + }) + + describe('when only ETag matches', function () { + it('should be stale', function () { + var reqHeaders = { 'if-none-match': '"foo"', 'if-modified-since': 'Sat, 01 Jan 2000 00:00:00 GMT' } + var resHeaders = { 'etag': '"foo"', 'last-modified': 'Sat, 01 Jan 2000 01:00:00 GMT' } + assert.ok(!fresh(reqHeaders, resHeaders)) + }) + }) + + describe('when only Last-Modified matches', function () { + it('should be stale', function () { + var reqHeaders = { 'if-none-match': '"foo"', 'if-modified-since': 'Sat, 01 Jan 2000 01:00:00 GMT' } + var resHeaders = { 'etag': '"bar"', 'last-modified': 'Sat, 01 Jan 2000 00:00:00 GMT' } + assert.ok(!fresh(reqHeaders, resHeaders)) + }) + }) + + describe('when none match', function () { + it('should be stale', function () { + var reqHeaders = { 'if-none-match': '"foo"', 'if-modified-since': 'Sat, 01 Jan 2000 00:00:00 GMT' } + var resHeaders = { 'etag': '"bar"', 'last-modified': 'Sat, 01 Jan 2000 01:00:00 GMT' } + assert.ok(!fresh(reqHeaders, resHeaders)) + }) + }) + }) + + describe('when requested with Cache-Control: no-cache', function () { + it('should be stale', function () { + var reqHeaders = { 'cache-control': ' no-cache' } + var resHeaders = {} + assert.ok(!fresh(reqHeaders, resHeaders)) + }) + + describe('when ETags match', function () { + it('should be stale', function () { + var reqHeaders = { 'cache-control': ' no-cache', 'if-none-match': '"foo"' } + var resHeaders = { 'etag': '"foo"' } + assert.ok(!fresh(reqHeaders, resHeaders)) + }) + }) + + describe('when unmodified since the date', function () { + it('should be stale', function () { + var reqHeaders = { 'cache-control': ' no-cache', 'if-modified-since': 'Sat, 01 Jan 2000 01:00:00 GMT' } + var resHeaders = { 'last-modified': 'Sat, 01 Jan 2000 00:00:00 GMT' } + assert.ok(!fresh(reqHeaders, resHeaders)) + }) + }) + }) +}) diff --git a/test/response/content.js b/test/response/content.js new file mode 100644 index 0000000..943bc77 --- /dev/null +++ b/test/response/content.js @@ -0,0 +1,962 @@ + +'use strict'; + +const assert = require('assert'); +const contentDisposition = require('../../lib/content-disposition') + +describe('contentDisposition()', function () { + it('should create an attachment header', function () { + assert.strictEqual(contentDisposition(), 'attachment') + }) +}) + +describe('contentDisposition(filename)', function () { + it('should require a string', function () { + assert.throws(contentDisposition.bind(null, 42), + /filename.*string/) + }) + + it('should create a header with file name', function () { + assert.strictEqual(contentDisposition('plans.pdf'), + 'attachment; filename="plans.pdf"') + }) + + it('should use the basename of the string', function () { + assert.strictEqual(contentDisposition('/path/to/plans.pdf'), + 'attachment; filename="plans.pdf"') + }) + + describe('when "filename" is US-ASCII', function () { + it('should only include filename parameter', function () { + assert.strictEqual(contentDisposition('plans.pdf'), + 'attachment; filename="plans.pdf"') + }) + + it('should escape quotes', function () { + assert.strictEqual(contentDisposition('the "plans".pdf'), + 'attachment; filename="the \\"plans\\".pdf"') + }) + }) + + describe('when "filename" is ISO-8859-1', function () { + it('should only include filename parameter', function () { + assert.strictEqual(contentDisposition('«plans».pdf'), + 'attachment; filename="«plans».pdf"') + }) + + it('should escape quotes', function () { + assert.strictEqual(contentDisposition('the "plans" (1µ).pdf'), + 'attachment; filename="the \\"plans\\" (1µ).pdf"') + }) + }) + + describe('when "filename" is Unicode', function () { + it('should include filename* parameter', function () { + assert.strictEqual(contentDisposition('планы.pdf'), + 'attachment; filename="?????.pdf"; filename*=UTF-8\'\'%D0%BF%D0%BB%D0%B0%D0%BD%D1%8B.pdf') + }) + + it('should include filename fallback', function () { + assert.strictEqual(contentDisposition('£ and € rates.pdf'), + 'attachment; filename="£ and ? rates.pdf"; filename*=UTF-8\'\'%C2%A3%20and%20%E2%82%AC%20rates.pdf') + assert.strictEqual(contentDisposition('€ rates.pdf'), + 'attachment; filename="? rates.pdf"; filename*=UTF-8\'\'%E2%82%AC%20rates.pdf') + }) + + it('should encode special characters', function () { + assert.strictEqual(contentDisposition('€\'*%().pdf'), + 'attachment; filename="?\'*%().pdf"; filename*=UTF-8\'\'%E2%82%AC%27%2A%25%28%29.pdf') + }) + }) + + describe('when "filename" contains hex escape', function () { + it('should include filename* parameter', function () { + assert.strictEqual(contentDisposition('the%20plans.pdf'), + 'attachment; filename="the%20plans.pdf"; filename*=UTF-8\'\'the%2520plans.pdf') + }) + + it('should handle Unicode', function () { + assert.strictEqual(contentDisposition('€%20£.pdf'), + 'attachment; filename="?%20£.pdf"; filename*=UTF-8\'\'%E2%82%AC%2520%C2%A3.pdf') + }) + }) +}) + +describe('contentDisposition(filename, options)', function () { + describe('with "fallback" option', function () { + it('should require a string or Boolean', function () { + assert.throws(contentDisposition.bind(null, 'plans.pdf', { fallback: 42 }), + /fallback.*string/) + }) + + it('should default to true', function () { + assert.strictEqual(contentDisposition('€ rates.pdf'), + 'attachment; filename="? rates.pdf"; filename*=UTF-8\'\'%E2%82%AC%20rates.pdf') + }) + + describe('when "false"', function () { + it('should not generate ISO-8859-1 fallback', function () { + assert.strictEqual(contentDisposition('£ and € rates.pdf', { fallback: false }), + 'attachment; filename*=UTF-8\'\'%C2%A3%20and%20%E2%82%AC%20rates.pdf') + }) + + it('should keep ISO-8859-1 filename', function () { + assert.strictEqual(contentDisposition('£ rates.pdf', { fallback: false }), + 'attachment; filename="£ rates.pdf"') + }) + }) + + describe('when "true"', function () { + it('should generate ISO-8859-1 fallback', function () { + assert.strictEqual(contentDisposition('£ and € rates.pdf', { fallback: true }), + 'attachment; filename="£ and ? rates.pdf"; filename*=UTF-8\'\'%C2%A3%20and%20%E2%82%AC%20rates.pdf') + }) + + it('should pass through ISO-8859-1 filename', function () { + assert.strictEqual(contentDisposition('£ rates.pdf', { fallback: true }), + 'attachment; filename="£ rates.pdf"') + }) + }) + + describe('when a string', function () { + it('should require an ISO-8859-1 string', function () { + assert.throws(contentDisposition.bind(null, '€ rates.pdf', { fallback: '€ rates.pdf' }), + /fallback.*iso-8859-1/i) + }) + + it('should use as ISO-8859-1 fallback', function () { + assert.strictEqual(contentDisposition('£ and € rates.pdf', { fallback: '£ and EURO rates.pdf' }), + 'attachment; filename="£ and EURO rates.pdf"; filename*=UTF-8\'\'%C2%A3%20and%20%E2%82%AC%20rates.pdf') + }) + + it('should use as fallback even when filename is ISO-8859-1', function () { + assert.strictEqual(contentDisposition('"£ rates".pdf', { fallback: '£ rates.pdf' }), + 'attachment; filename="£ rates.pdf"; filename*=UTF-8\'\'%22%C2%A3%20rates%22.pdf') + }) + + it('should do nothing if equal to filename', function () { + assert.strictEqual(contentDisposition('plans.pdf', { fallback: 'plans.pdf' }), + 'attachment; filename="plans.pdf"') + }) + + it('should use the basename of the string', function () { + assert.strictEqual(contentDisposition('€ rates.pdf', { fallback: '/path/to/EURO rates.pdf' }), + 'attachment; filename="EURO rates.pdf"; filename*=UTF-8\'\'%E2%82%AC%20rates.pdf') + }) + + it('should do nothing without filename option', function () { + assert.strictEqual(contentDisposition(undefined, { fallback: 'plans.pdf' }), + 'attachment') + }) + }) + }) + + describe('with "type" option', function () { + it('should default to attachment', function () { + assert.strictEqual(contentDisposition(), + 'attachment') + }) + + it('should require a string', function () { + assert.throws(contentDisposition.bind(null, undefined, { type: 42 }), + /invalid type/) + }) + + it('should require a valid type', function () { + assert.throws(contentDisposition.bind(null, undefined, { type: 'invlaid;type' }), + /invalid type/) + }) + + it('should create a header with inline type', function () { + assert.strictEqual(contentDisposition(undefined, { type: 'inline' }), + 'inline') + }) + + it('should create a header with inline type & filename', function () { + assert.strictEqual(contentDisposition('plans.pdf', { type: 'inline' }), + 'inline; filename="plans.pdf"') + }) + + it('should normalize type', function () { + assert.strictEqual(contentDisposition(undefined, { type: 'INLINE' }), + 'inline') + }) + }) +}) + +describe('contentDisposition.parse(string)', function () { + it('should require string', function () { + assert.throws(contentDisposition.parse.bind(null), /argument string.*required/) + }) + + it('should reject non-strings', function () { + assert.throws(contentDisposition.parse.bind(null, 42), /argument string.*required/) + }) + + describe('with only type', function () { + it('should reject quoted value', function () { + assert.throws(contentDisposition.parse.bind(null, '"attachment"'), + /invalid type format/) + }) + + it('should reject trailing semicolon', function () { + assert.throws(contentDisposition.parse.bind(null, 'attachment;'), + /invalid.*format/) + }) + + it('should parse "attachment"', function () { + assert.deepEqual(contentDisposition.parse('attachment'), { + type: 'attachment', + parameters: {} + }) + }) + + it('should parse "inline"', function () { + assert.deepEqual(contentDisposition.parse('inline'), { + type: 'inline', + parameters: {} + }) + }) + + it('should parse "form-data"', function () { + assert.deepEqual(contentDisposition.parse('form-data'), { + type: 'form-data', + parameters: {} + }) + }) + + it('should parse with trailing LWS', function () { + assert.deepEqual(contentDisposition.parse('attachment \t '), { + type: 'attachment', + parameters: {} + }) + }) + + it('should normalize to lower-case', function () { + assert.deepEqual(contentDisposition.parse('ATTACHMENT'), { + type: 'attachment', + parameters: {} + }) + }) + }) + + describe('with parameters', function () { + it('should reject trailing semicolon', function () { + assert.throws(contentDisposition.parse.bind(null, 'attachment; filename="rates.pdf";'), + /invalid parameter format/) + }) + + it('should reject invalid parameter name', function () { + assert.throws(contentDisposition.parse.bind(null, 'attachment; filename@="rates.pdf"'), + /invalid parameter format/) + }) + + it('should reject missing parameter value', function () { + assert.throws(contentDisposition.parse.bind(null, 'attachment; filename='), + /invalid parameter format/) + }) + + it('should reject invalid parameter value', function () { + assert.throws(contentDisposition.parse.bind(null, 'attachment; filename=trolly,trains'), + /invalid parameter format/) + }) + + it('should reject invalid parameters', function () { + assert.throws(contentDisposition.parse.bind(null, 'attachment; filename=total/; foo=bar'), + /invalid parameter format/) + }) + + it('should reject duplicate parameters', function () { + assert.throws(contentDisposition.parse.bind(null, 'attachment; filename=foo; filename=bar'), + /invalid duplicate parameter/) + }) + + it('should reject missing type', function () { + assert.throws(contentDisposition.parse.bind(null, 'filename="plans.pdf"'), + /invalid type format/) + assert.throws(contentDisposition.parse.bind(null, '; filename="plans.pdf"'), + /invalid type format/) + }) + + it('should lower-case parameter name', function () { + assert.deepEqual(contentDisposition.parse('attachment; FILENAME="plans.pdf"'), { + type: 'attachment', + parameters: { filename: 'plans.pdf' } + }) + }) + + it('should parse quoted parameter value', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename="plans.pdf"'), { + type: 'attachment', + parameters: { filename: 'plans.pdf' } + }) + }) + + it('should parse & unescape quoted value', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename="the \\"plans\\".pdf"'), { + type: 'attachment', + parameters: { filename: 'the "plans".pdf' } + }) + }) + + it('should include all parameters', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename="plans.pdf"; foo=bar'), { + type: 'attachment', + parameters: { filename: 'plans.pdf', foo: 'bar' } + }) + }) + + it('should parse parameters separated with any LWS', function () { + assert.deepEqual(contentDisposition.parse('attachment;filename="plans.pdf" \t; \t\t foo=bar'), { + type: 'attachment', + parameters: { filename: 'plans.pdf', foo: 'bar' } + }) + }) + + it('should parse token filename', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename=plans.pdf'), { + type: 'attachment', + parameters: { filename: 'plans.pdf' } + }) + }) + + it('should parse ISO-8859-1 filename', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename="£ rates.pdf"'), { + type: 'attachment', + parameters: { filename: '£ rates.pdf' } + }) + }) + }) + + describe('with extended parameters', function () { + it('should reject quoted extended parameter value', function () { + assert.throws(contentDisposition.parse.bind(null, 'attachment; filename*="UTF-8\'\'%E2%82%AC%20rates.pdf"'), + /invalid extended.*value/) + }) + + it('should parse UTF-8 extended parameter value', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename*=UTF-8\'\'%E2%82%AC%20rates.pdf'), { + type: 'attachment', + parameters: { 'filename': '€ rates.pdf' } + }) + }) + + it('should parse UTF-8 extended parameter value', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename*=UTF-8\'\'%E2%82%AC%20rates.pdf'), { + type: 'attachment', + parameters: { 'filename': '€ rates.pdf' } + }) + assert.deepEqual(contentDisposition.parse('attachment; filename*=UTF-8\'\'%E4%20rates.pdf'), { + type: 'attachment', + parameters: { 'filename': '\ufffd rates.pdf' } + }) + }) + + it('should parse ISO-8859-1 extended parameter value', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename*=ISO-8859-1\'\'%A3%20rates.pdf'), { + type: 'attachment', + parameters: { 'filename': '£ rates.pdf' } + }) + assert.deepEqual(contentDisposition.parse('attachment; filename*=ISO-8859-1\'\'%82%20rates.pdf'), { + type: 'attachment', + parameters: { 'filename': '? rates.pdf' } + }) + }) + + it('should not be case-sensitive for charser', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename*=utf-8\'\'%E2%82%AC%20rates.pdf'), { + type: 'attachment', + parameters: { 'filename': '€ rates.pdf' } + }) + }) + + it('should reject unsupported charset', function () { + assert.throws(contentDisposition.parse.bind(null, 'attachment; filename*=ISO-8859-2\'\'%A4%20rates.pdf'), + /unsupported charset/) + }) + + it('should parse with embedded language', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename*=UTF-8\'en\'%E2%82%AC%20rates.pdf'), { + type: 'attachment', + parameters: { 'filename': '€ rates.pdf' } + }) + }) + + it('should prefer extended parameter value', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename="EURO rates.pdf"; filename*=UTF-8\'\'%E2%82%AC%20rates.pdf'), { + type: 'attachment', + parameters: { 'filename': '€ rates.pdf' } + }) + assert.deepEqual(contentDisposition.parse('attachment; filename*=UTF-8\'\'%E2%82%AC%20rates.pdf; filename="EURO rates.pdf"'), { + type: 'attachment', + parameters: { 'filename': '€ rates.pdf' } + }) + }) + }) + + describe('from TC 2231', function () { + describe('Disposition-Type Inline', function () { + it('should parse "inline"', function () { + assert.deepEqual(contentDisposition.parse('inline'), { + type: 'inline', + parameters: {} + }) + }) + + it('should reject ""inline""', function () { + assert.throws(contentDisposition.parse.bind(null, '"inline"'), + /invalid type format/) + }) + + it('should parse "inline; filename="foo.html""', function () { + assert.deepEqual(contentDisposition.parse('inline; filename="foo.html"'), { + type: 'inline', + parameters: { filename: 'foo.html' } + }) + }) + + it('should parse "inline; filename="Not an attachment!""', function () { + assert.deepEqual(contentDisposition.parse('inline; filename="Not an attachment!"'), { + type: 'inline', + parameters: { filename: 'Not an attachment!' } + }) + }) + + it('should parse "inline; filename="foo.pdf""', function () { + assert.deepEqual(contentDisposition.parse('inline; filename="foo.pdf"'), { + type: 'inline', + parameters: { filename: 'foo.pdf' } + }) + }) + }) + + describe('Disposition-Type Attachment', function () { + it('should parse "attachment"', function () { + assert.deepEqual(contentDisposition.parse('attachment'), { + type: 'attachment', + parameters: {} + }) + }) + + it('should reject ""attachment""', function () { + assert.throws(contentDisposition.parse.bind(null, '"attachment"'), + /invalid type format/) + }) + + it('should parse "ATTACHMENT"', function () { + assert.deepEqual(contentDisposition.parse('ATTACHMENT'), { + type: 'attachment', + parameters: {} + }) + }) + + it('should parse "attachment; filename="foo.html""', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename="foo.html"'), { + type: 'attachment', + parameters: { filename: 'foo.html' } + }) + }) + + it('should parse "attachment; filename="0000000000111111111122222""', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename="0000000000111111111122222"'), { + type: 'attachment', + parameters: { filename: '0000000000111111111122222' } + }) + }) + + it('should parse "attachment; filename="00000000001111111111222222222233333""', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename="00000000001111111111222222222233333"'), { + type: 'attachment', + parameters: { filename: '00000000001111111111222222222233333' } + }) + }) + + it('should parse "attachment; filename="f\\oo.html""', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename="f\\oo.html"'), { + type: 'attachment', + parameters: { filename: 'foo.html' } + }) + }) + + it('should parse "attachment; filename="\\"quoting\\" tested.html""', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename="\\"quoting\\" tested.html"'), { + type: 'attachment', + parameters: { filename: '"quoting" tested.html' } + }) + }) + + it('should parse "attachment; filename="Here\'s a semicolon;.html""', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename="Here\'s a semicolon;.html"'), { + type: 'attachment', + parameters: { filename: 'Here\'s a semicolon;.html' } + }) + }) + + it('should parse "attachment; foo="bar"; filename="foo.html""', function () { + assert.deepEqual(contentDisposition.parse('attachment; foo="bar"; filename="foo.html"'), { + type: 'attachment', + parameters: { filename: 'foo.html', foo: 'bar' } + }) + }) + + it('should parse "attachment; foo="\\"\\\\";filename="foo.html""', function () { + assert.deepEqual(contentDisposition.parse('attachment; foo="\\"\\\\";filename="foo.html"'), { + type: 'attachment', + parameters: { filename: 'foo.html', foo: '"\\' } + }) + }) + + it('should parse "attachment; FILENAME="foo.html""', function () { + assert.deepEqual(contentDisposition.parse('attachment; FILENAME="foo.html"'), { + type: 'attachment', + parameters: { filename: 'foo.html' } + }) + }) + + it('should parse "attachment; filename=foo.html"', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename=foo.html'), { + type: 'attachment', + parameters: { filename: 'foo.html' } + }) + }) + + it('should reject "attachment; filename=foo,bar.html"', function () { + assert.throws(contentDisposition.parse.bind(null, 'attachment; filename=foo,bar.html'), + /invalid parameter format/) + }) + + it('should reject "attachment; filename=foo.html ;"', function () { + assert.throws(contentDisposition.parse.bind(null, 'attachment; filename=foo.html ;'), + /invalid parameter format/) + }) + + it('should reject "attachment; ;filename=foo"', function () { + assert.throws(contentDisposition.parse.bind(null, 'attachment; ;filename=foo'), + /invalid parameter format/) + }) + + it('should reject "attachment; filename=foo bar.html"', function () { + assert.throws(contentDisposition.parse.bind(null, 'attachment; filename=foo bar.html'), + /invalid parameter format/) + }) + + it('should parse "attachment; filename=\'foo.bar\'', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename=\'foo.bar\''), { + type: 'attachment', + parameters: { filename: '\'foo.bar\'' } + }) + }) + + it('should parse "attachment; filename="foo-ä.html""', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename="foo-ä.html"'), { + type: 'attachment', + parameters: { filename: 'foo-ä.html' } + }) + }) + + it('should parse "attachment; filename="foo-ä.html""', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename="foo-ä.html"'), { + type: 'attachment', + parameters: { filename: 'foo-ä.html' } + }) + }) + + it('should parse "attachment; filename="foo-%41.html""', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename="foo-%41.html"'), { + type: 'attachment', + parameters: { filename: 'foo-%41.html' } + }) + }) + + it('should parse "attachment; filename="50%.html""', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename="50%.html"'), { + type: 'attachment', + parameters: { filename: '50%.html' } + }) + }) + + it('should parse "attachment; filename="foo-%\\41.html""', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename="foo-%\\41.html"'), { + type: 'attachment', + parameters: { filename: 'foo-%41.html' } + }) + }) + + it('should parse "attachment; name="foo-%41.html""', function () { + assert.deepEqual(contentDisposition.parse('attachment; name="foo-%41.html"'), { + type: 'attachment', + parameters: { name: 'foo-%41.html' } + }) + }) + + it('should parse "attachment; filename="ä-%41.html""', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename="ä-%41.html"'), { + type: 'attachment', + parameters: { filename: 'ä-%41.html' } + }) + }) + + it('should parse "attachment; filename="foo-%c3%a4-%e2%82%ac.html""', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename="foo-%c3%a4-%e2%82%ac.html"'), { + type: 'attachment', + parameters: { filename: 'foo-%c3%a4-%e2%82%ac.html' } + }) + }) + + it('should parse "attachment; filename ="foo.html""', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename ="foo.html"'), { + type: 'attachment', + parameters: { filename: 'foo.html' } + }) + }) + + it('should reject "attachment; filename="foo.html"; filename="bar.html"', function () { + assert.throws(contentDisposition.parse.bind(null, 'attachment; filename="foo.html"; filename="bar.html"'), + /invalid duplicate parameter/) + }) + + it('should reject "attachment; filename=foo[1](2).html"', function () { + assert.throws(contentDisposition.parse.bind(null, 'attachment; filename=foo[1](2).html'), + /invalid parameter format/) + }) + + it('should reject "attachment; filename=foo-ä.html"', function () { + assert.throws(contentDisposition.parse.bind(null, 'attachment; filename=foo-ä.html'), + /invalid parameter format/) + }) + + it('should reject "attachment; filename=foo-ä.html"', function () { + assert.throws(contentDisposition.parse.bind(null, 'attachment; filename=foo-ä.html'), + /invalid parameter format/) + }) + + it('should reject "filename=foo.html"', function () { + assert.throws(contentDisposition.parse.bind(null, 'filename=foo.html'), + /invalid type format/) + }) + + it('should reject "x=y; filename=foo.html"', function () { + assert.throws(contentDisposition.parse.bind(null, 'x=y; filename=foo.html'), + /invalid type format/) + }) + + it('should reject ""foo; filename=bar;baz"; filename=qux"', function () { + assert.throws(contentDisposition.parse.bind(null, '"foo; filename=bar;baz"; filename=qux'), + /invalid type format/) + }) + + it('should reject "filename=foo.html, filename=bar.html"', function () { + assert.throws(contentDisposition.parse.bind(null, 'filename=foo.html, filename=bar.html'), + /invalid type format/) + }) + + it('should reject "; filename=foo.html"', function () { + assert.throws(contentDisposition.parse.bind(null, '; filename=foo.html'), + /invalid type format/) + }) + + it('should reject ": inline; attachment; filename=foo.html', function () { + assert.throws(contentDisposition.parse.bind(null, ': inline; attachment; filename=foo.html'), + /invalid type format/) + }) + + it('should reject "inline; attachment; filename=foo.html', function () { + assert.throws(contentDisposition.parse.bind(null, 'inline; attachment; filename=foo.html'), + /invalid parameter format/) + }) + + it('should reject "attachment; inline; filename=foo.html', function () { + assert.throws(contentDisposition.parse.bind(null, 'attachment; inline; filename=foo.html'), + /invalid parameter format/) + }) + + it('should reject "attachment; filename="foo.html".txt', function () { + assert.throws(contentDisposition.parse.bind(null, 'attachment; filename="foo.html".txt'), + /invalid parameter format/) + }) + + it('should reject "attachment; filename="bar', function () { + assert.throws(contentDisposition.parse.bind(null, 'attachment; filename="bar'), + /invalid parameter format/) + }) + + it('should reject "attachment; filename=foo"bar;baz"qux', function () { + assert.throws(contentDisposition.parse.bind(null, 'attachment; filename=foo"bar;baz"qux'), + /invalid parameter format/) + }) + + it('should reject "attachment; filename=foo.html, attachment; filename=bar.html', function () { + assert.throws(contentDisposition.parse.bind(null, 'attachment; filename=foo.html, attachment; filename=bar.html'), + /invalid parameter format/) + }) + + it('should reject "attachment; foo=foo filename=bar', function () { + assert.throws(contentDisposition.parse.bind(null, 'attachment; foo=foo filename=bar'), + /invalid parameter format/) + }) + + it('should reject "attachment; filename=bar foo=foo', function () { + assert.throws(contentDisposition.parse.bind(null, 'attachment; filename=bar foo=foo'), + /invalid parameter format/) + }) + + it('should reject "attachment filename=bar', function () { + assert.throws(contentDisposition.parse.bind(null, 'attachment filename=bar'), + /invalid type format/) + }) + + it('should reject "filename=foo.html; attachment', function () { + assert.throws(contentDisposition.parse.bind(null, 'filename=foo.html; attachment'), + /invalid type format/) + }) + + it('should parse "attachment; xfilename=foo.html"', function () { + assert.deepEqual(contentDisposition.parse('attachment; xfilename=foo.html'), { + type: 'attachment', + parameters: { xfilename: 'foo.html' } + }) + }) + + it('should parse "attachment; filename="/foo.html""', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename="/foo.html"'), { + type: 'attachment', + parameters: { filename: '/foo.html' } + }) + }) + + it('should parse "attachment; filename="\\\\foo.html""', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename="\\\\foo.html"'), { + type: 'attachment', + parameters: { filename: '\\foo.html' } + }) + }) + }) + + describe('Additional Parameters', function () { + it('should parse "attachment; creation-date="Wed, 12 Feb 1997 16:29:51 -0500""', function () { + assert.deepEqual(contentDisposition.parse('attachment; creation-date="Wed, 12 Feb 1997 16:29:51 -0500"'), { + type: 'attachment', + parameters: { 'creation-date': 'Wed, 12 Feb 1997 16:29:51 -0500' } + }) + }) + + it('should parse "attachment; modification-date="Wed, 12 Feb 1997 16:29:51 -0500""', function () { + assert.deepEqual(contentDisposition.parse('attachment; modification-date="Wed, 12 Feb 1997 16:29:51 -0500"'), { + type: 'attachment', + parameters: { 'modification-date': 'Wed, 12 Feb 1997 16:29:51 -0500' } + }) + }) + }) + + describe('Disposition-Type Extension', function () { + it('should parse "foobar"', function () { + assert.deepEqual(contentDisposition.parse('foobar'), { + type: 'foobar', + parameters: {} + }) + }) + + it('should parse "attachment; example="filename=example.txt""', function () { + assert.deepEqual(contentDisposition.parse('attachment; example="filename=example.txt"'), { + type: 'attachment', + parameters: { example: 'filename=example.txt' } + }) + }) + }) + + describe('RFC 2231/5987 Encoding: Character Sets', function () { + it('should parse "attachment; filename*=iso-8859-1\'\'foo-%E4.html"', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename*=iso-8859-1\'\'foo-%E4.html'), { + type: 'attachment', + parameters: { filename: 'foo-ä.html' } + }) + }) + + it('should parse "attachment; filename*=UTF-8\'\'foo-%c3%a4-%e2%82%ac.html"', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename*=UTF-8\'\'foo-%c3%a4-%e2%82%ac.html'), { + type: 'attachment', + parameters: { filename: 'foo-ä-€.html' } + }) + }) + + it('should reject "attachment; filename*=\'\'foo-%c3%a4-%e2%82%ac.html"', function () { + assert.throws(contentDisposition.parse.bind(null, 'attachment; filename*=\'\'foo-%c3%a4-%e2%82%ac.html'), + /invalid extended.*value/) + }) + + it('should parse "attachment; filename*=UTF-8\'\'foo-a%cc%88.html"', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename*=UTF-8\'\'foo-a%cc%88.html'), { + type: 'attachment', + parameters: { filename: 'foo-ä.html' } + }) + }) + + it('should parse "attachment; filename*=iso-8859-1\'\'foo-%c3%a4-%e2%82%ac.html"', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename*=iso-8859-1\'\'foo-%c3%a4-%e2%82%ac.html'), { + type: 'attachment', + parameters: { filename: 'foo-ä-â?¬.html' } + }) + }) + + it('should parse "attachment; filename*=utf-8\'\'foo-%E4.html"', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename*=utf-8\'\'foo-%E4.html'), { + type: 'attachment', + parameters: { filename: 'foo-\ufffd.html' } + }) + }) + + it('should reject "attachment; filename *=UTF-8\'\'foo-%c3%a4.html"', function () { + assert.throws(contentDisposition.parse.bind(null, 'attachment; filename *=UTF-8\'\'foo-%c3%a4.html'), + /invalid parameter format/) + }) + + it('should parse "attachment; filename*= UTF-8\'\'foo-%c3%a4.html"', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename*= UTF-8\'\'foo-%c3%a4.html'), { + type: 'attachment', + parameters: { filename: 'foo-ä.html' } + }) + }) + + it('should parse "attachment; filename* =UTF-8\'\'foo-%c3%a4.html"', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename* =UTF-8\'\'foo-%c3%a4.html'), { + type: 'attachment', + parameters: { filename: 'foo-ä.html' } + }) + }) + + it('should reject "attachment; filename*="UTF-8\'\'foo-%c3%a4.html""', function () { + assert.throws(contentDisposition.parse.bind(null, 'attachment; filename*="UTF-8\'\'foo-%c3%a4.html"'), + /invalid extended field value/) + }) + + it('should reject "attachment; filename*="foo%20bar.html""', function () { + assert.throws(contentDisposition.parse.bind(null, 'attachment; filename*="foo%20bar.html"'), + /invalid extended field value/) + }) + + it('should reject "attachment; filename*=UTF-8\'foo-%c3%a4.html"', function () { + assert.throws(contentDisposition.parse.bind(null, 'attachment; filename*=UTF-8\'foo-%c3%a4.html'), + /invalid extended field value/) + }) + + it('should reject "attachment; filename*=UTF-8\'\'foo%"', function () { + assert.throws(contentDisposition.parse.bind(null, 'attachment; filename*=UTF-8\'\'foo%'), + /invalid extended field value/) + }) + + it('should reject "attachment; filename*=UTF-8\'\'f%oo.html"', function () { + assert.throws(contentDisposition.parse.bind(null, 'attachment; filename*=UTF-8\'\'f%oo.html'), + /invalid extended field value/) + }) + + it('should parse "attachment; filename*=UTF-8\'\'A-%2541.html"', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename*=UTF-8\'\'A-%2541.html'), { + type: 'attachment', + parameters: { filename: 'A-%41.html' } + }) + }) + + it('should parse "attachment; filename*=UTF-8\'\'%5cfoo.html"', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename*=UTF-8\'\'%5cfoo.html'), { + type: 'attachment', + parameters: { filename: '\\foo.html' } + }) + }) + }) + + describe('RFC2231 Encoding: Continuations', function () { + it('should parse "attachment; filename*0="foo."; filename*1="html""', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename*0="foo."; filename*1="html"'), { + type: 'attachment', + parameters: { 'filename*0': 'foo.', 'filename*1': 'html' } + }) + }) + + it('should parse "attachment; filename*0="foo"; filename*1="\\b\\a\\r.html""', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename*0="foo"; filename*1="\\b\\a\\r.html"'), { + type: 'attachment', + parameters: { 'filename*0': 'foo', 'filename*1': 'bar.html' } + }) + }) + + it('should parse "attachment; filename*0*=UTF-8\'\'foo-%c3%a4; filename*1=".html""', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename*0*=UTF-8\'\'foo-%c3%a4; filename*1=".html"'), { + type: 'attachment', + parameters: { 'filename*0*': 'UTF-8\'\'foo-%c3%a4', 'filename*1': '.html' } + }) + }) + + it('should parse "attachment; filename*0="foo"; filename*01="bar""', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename*0="foo"; filename*01="bar"'), { + type: 'attachment', + parameters: { 'filename*0': 'foo', 'filename*01': 'bar' } + }) + }) + + it('should parse "attachment; filename*0="foo"; filename*2="bar""', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename*0="foo"; filename*2="bar"'), { + type: 'attachment', + parameters: { 'filename*0': 'foo', 'filename*2': 'bar' } + }) + }) + + it('should parse "attachment; filename*1="foo."; filename*2="html""', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename*1="foo."; filename*2="html"'), { + type: 'attachment', + parameters: { 'filename*1': 'foo.', 'filename*2': 'html' } + }) + }) + + it('should parse "attachment; filename*1="bar"; filename*0="foo""', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename*1="bar"; filename*0="foo"'), { + type: 'attachment', + parameters: { 'filename*1': 'bar', 'filename*0': 'foo' } + }) + }) + }) + + describe('RFC2231 Encoding: Fallback Behaviour', function () { + it('should parse "attachment; filename="foo-ae.html"; filename*=UTF-8\'\'foo-%c3%a4.html"', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename="foo-ae.html"; filename*=UTF-8\'\'foo-%c3%a4.html'), { + type: 'attachment', + parameters: { filename: 'foo-ä.html' } + }) + }) + + it('should parse "attachment; filename*=UTF-8\'\'foo-%c3%a4.html; filename="foo-ae.html"', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename*=UTF-8\'\'foo-%c3%a4.html; filename="foo-ae.html"'), { + type: 'attachment', + parameters: { filename: 'foo-ä.html' } + }) + }) + + it('should parse "attachment; filename*0*=ISO-8859-15\'\'euro-sign%3d%a4; filename*=ISO-8859-1\'\'currency-sign%3d%a4', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename*0*=ISO-8859-15\'\'euro-sign%3d%a4; filename*=ISO-8859-1\'\'currency-sign%3d%a4'), { + type: 'attachment', + parameters: { filename: 'currency-sign=¤', 'filename*0*': 'ISO-8859-15\'\'euro-sign%3d%a4' } + }) + }) + + it('should parse "attachment; foobar=x; filename="foo.html"', function () { + assert.deepEqual(contentDisposition.parse('attachment; foobar=x; filename="foo.html"'), { + type: 'attachment', + parameters: { filename: 'foo.html', foobar: 'x' } + }) + }) + }) + + describe('RFC2047 Encoding', function () { + it('should reject "attachment; filename==?ISO-8859-1?Q?foo-=E4.html?="', function () { + assert.throws(contentDisposition.parse.bind(null, 'attachment; filename==?ISO-8859-1?Q?foo-=E4.html?='), + /invalid parameter format/) + }) + + it('should parse "attachment; filename="=?ISO-8859-1?Q?foo-=E4.html?=""', function () { + assert.deepEqual(contentDisposition.parse('attachment; filename="=?ISO-8859-1?Q?foo-=E4.html?="'), { + type: 'attachment', + parameters: { filename: '=?ISO-8859-1?Q?foo-=E4.html?=' } + }) + }) + }) + }) +}) \ No newline at end of file