Skip to content

Commit

Permalink
Expand non-Unicode filename to the full ISO-8859-1 charset
Browse files Browse the repository at this point in the history
  • Loading branch information
dougwilson committed Sep 21, 2014
1 parent f347389 commit df2e5df
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 67 deletions.
5 changes: 5 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
unreleased
==========

* Expand non-Unicode `filename` to the full ISO-8859-1 charset

0.3.0 / 2014-09-20
==================

Expand Down
22 changes: 13 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,28 +30,32 @@ want to specify `options`, set `filename` to `undefined`.
res.setHeader('Content-Disposition', contentDisposition('∫ maths.pdf'))
```

**note** HTTP headers are of the ISO-8859-1 character set. If you are writing this
header through a means different from `setHeader` in Node.js, you'll want to specify
the `'binary'` encoding in Node.js.

#### Options

`contentDisposition` accepts these properties in the options object.

##### fallback

If the `filename` option is outside US-ASCII, then the file name is actually
If the `filename` option is outside ISO-8859-1, then the file name is actually
stored in a supplemental field for clients that support Unicode file names and
a US-ASCII version of the file name is automatically generated.
a ISO-8859-1 version of the file name is automatically generated.

This specifies the US-ASCII file name to override the automatic generation or
This specifies the ISO-8859-1 file name to override the automatic generation or
disables the generation all together, defaults to `true`.

- A string will specify the US-ASCII file name to use in place of automatic
- A string will specify the ISO-8859-1 file name to use in place of automatic
generation.
- `false` will disable including a US-ASCII file name and only include the
Unicode version (unless the file name is already US-ASCII).
- `true` will enable automatic generation if the file name is outside US-ASCII.
- `false` will disable including a ISO-8859-1 file name and only include the
Unicode version (unless the file name is already ISO-8859-1).
- `true` will enable automatic generation if the file name is outside ISO-8859-1.

If the `filename` option is US-ASCII and this option is specified and has a
If the `filename` option is ISO-8859-1 and this option is specified and has a
different value, then the `filename` option is encoded in the extended field
and this set as the fallback field, even though they are both US-ASCII.
and this set as the fallback field, even though they are both ISO-8859-1.

##### type

Expand Down
30 changes: 10 additions & 20 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,6 @@ module.exports = contentDisposition

var basename = require('path').basename

/**
* RegExp to match US-ASCII string
*/

var asciiStringRegExp = /^[\x00-\x7f]*$/

/**
* RegExp to match non attr-char, *after* encodeURIComponent (i.e. not including "%")
*/
Expand All @@ -35,10 +29,10 @@ var encodeUriAttrCharRegExp = /[\x00-\x20"'\(\)*,\/:;<=>?@\[\\\]\{\}\x7f]/g
var hexEscapeRegExp = /%[0-9A-F]{2}/i

/**
* RegExp to match non-US-ASCII characters.
* RegExp to match non-RFC 2616 text characters.
*/

var nonAsciiRegExp = /[^\x00-\x7f]/g
var nonTextRegExp = /[^\x20-\x7e\x80-\xff]/g

/**
* RegExp to match chars that must be quoted-pair in RFC 2616
Expand Down Expand Up @@ -109,21 +103,21 @@ function contentDisposition(filename, options) {
throw new TypeError('option fallback must be a string or boolean')
}

if (typeof fallback === 'string' && nonAsciiRegExp.test(fallback)) {
throw new TypeError('option fallback must be US-ASCII string')
if (typeof fallback === 'string' && nonTextRegExp.test(fallback)) {
throw new TypeError('option fallback must be ISO-8859-1 string')
}

// restrict to file base name
var name = basename(filename)

// generate fallback name
var fallbackName = typeof fallback !== 'string'
? fallback && getascii(name)
? fallback && getlatin1(name)
: basename(fallback)

var isSimpleHeader = (typeof fallbackName !== 'string' || fallbackName === name)
&& asciiStringRegExp.test(name)
&& !hexEscapeRegExp.test(name)
&& textRegExp.test(name)

if (isSimpleHeader) {
// simple header
Expand All @@ -137,16 +131,16 @@ function contentDisposition(filename, options) {
}

/**
* Get US-ASCII version of string.
* Get ISO-8859-1 version of string.
*
* @param {string} val
* @return {string}
* @api private
*/

function getascii(val) {
// simple Unicode -> US-ASCII transformation
return String(val).replace(nonAsciiRegExp, '?')
function getlatin1(val) {
// simple Unicode -> ISO-8859-1 transformation
return String(val).replace(nonTextRegExp, '?')
}

/**
Expand Down Expand Up @@ -178,10 +172,6 @@ function pencode(char) {
function qstring(val) {
var str = String(val)

if (str.length > 0 && !textRegExp.test(str)) {
throw new TypeError('invalid quoted string value')
}

return '"' + str.replace(quoteRegExp, '\\$1') + '"'
}

Expand Down
85 changes: 47 additions & 38 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,6 @@ describe('contentDisposition(filename)', function () {
'attachment; filename="plans.pdf"')
})

it('should not accept filename with NULLs', function () {
assert.throws(contentDisposition.bind(null, 'plans\u0000.pdf'),
/invalid.*value/)
})

describe('when "filename" is US-ASCII', function () {
it('should only include filename parameter', function () {
assert.equal(contentDisposition('plans.pdf'),
Expand All @@ -41,32 +36,46 @@ describe('contentDisposition(filename)', function () {
})
})

describe('when "filename" is ISO-8859-1', function () {
it('should only include filename parameter', function () {
assert.equal(contentDisposition('«plans».pdf'),
'attachment; filename="«plans».pdf"')
})

it('should escape quotes', function () {
assert.equal(contentDisposition('the "plans" (1µ).pdf'),
'attachment; filename="the \\"plans\\" (1µ).pdf"')
})
})

describe('when "filename" is Unicode', function () {
it('should include filename* parameter', function () {
assert.equal(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.equal(contentDisposition('«bye».pdf'),
'attachment; filename="?bye?.pdf"; filename*=UTF-8\'\'%C2%ABbye%C2%BB.pdf')
assert.equal(contentDisposition('«hi».pdf'),
'attachment; filename="?hi?.pdf"; filename*=UTF-8\'\'%C2%ABhi%C2%BB.pdf')
assert.equal(contentDisposition('£ and € rates.pdf'),
'attachment; filename="£ and ? rates.pdf"; filename*=UTF-8\'\'%C2%A3%20and%20%E2%82%AC%20rates.pdf')
assert.equal(contentDisposition('€ rates.pdf'),
'attachment; filename="? rates.pdf"; filename*=UTF-8\'\'%E2%82%AC%20rates.pdf')
})

it('should encode special characters', function () {
assert.equal(contentDisposition('«\'*%()».pdf'),
'attachment; filename="?\'*%()?.pdf"; filename*=UTF-8\'\'%C2%AB%27%2A%25%28%29%C2%BB.pdf')
assert.equal(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.equal(contentDisposition('the%20plans.pdf'), 'attachment; filename="the%20plans.pdf"; filename*=UTF-8\'\'the%2520plans.pdf')
assert.equal(contentDisposition('the%20plans.pdf'),
'attachment; filename="the%20plans.pdf"; filename*=UTF-8\'\'the%2520plans.pdf')
})

it('should handle Unicode', function () {
assert.equal(contentDisposition('«%20».pdf'), 'attachment; filename="?%20?.pdf"; filename*=UTF-8\'\'%C2%AB%2520%C2%BB.pdf')
assert.equal(contentDisposition('€%20£.pdf'),
'attachment; filename="?%20£.pdf"; filename*=UTF-8\'\'%E2%82%AC%2520%C2%A3.pdf')
})
})
})
Expand All @@ -79,48 +88,48 @@ describe('contentDisposition(filename, options)', function () {
})

it('should default to true', function () {
assert.equal(contentDisposition('«plans».pdf'),
'attachment; filename="?plans?.pdf"; filename*=UTF-8\'\'%C2%ABplans%C2%BB.pdf')
assert.equal(contentDisposition('€ rates.pdf'),
'attachment; filename="? rates.pdf"; filename*=UTF-8\'\'%E2%82%AC%20rates.pdf')
})

describe('when "false"', function () {
it('should not generate ASCII fallback', function () {
assert.equal(contentDisposition('«plans».pdf', { fallback: false }),
'attachment; filename*=UTF-8\'\'%C2%ABplans%C2%BB.pdf')
it('should not generate ISO-8859-1 fallback', function () {
assert.equal(contentDisposition('£ and € rates.pdf', { fallback: false }),
'attachment; filename*=UTF-8\'\'%C2%A3%20and%20%E2%82%AC%20rates.pdf')
})

it('should keep ASCII filename', function () {
assert.equal(contentDisposition('plans.pdf', { fallback: false }),
'attachment; filename="plans.pdf"')
it('should keep ISO-8859-1 filename', function () {
assert.equal(contentDisposition('£ rates.pdf', { fallback: false }),
'attachment; filename="£ rates.pdf"')
})
})

describe('when "true"', function () {
it('should generate ASCII fallback', function () {
assert.equal(contentDisposition('«plans».pdf', { fallback: true }),
'attachment; filename="?plans?.pdf"; filename*=UTF-8\'\'%C2%ABplans%C2%BB.pdf')
it('should generate ISO-8859-1 fallback', function () {
assert.equal(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 ASCII filename', function () {
assert.equal(contentDisposition('plans.pdf', { fallback: true }),
'attachment; filename="plans.pdf"')
it('should pass through ISO-8859-1 filename', function () {
assert.equal(contentDisposition('£ rates.pdf', { fallback: true }),
'attachment; filename="£ rates.pdf"')
})
})

describe('when a string', function () {
it('should require an ASCII string', function () {
assert.throws(contentDisposition.bind(null, '«plans».pdf', { fallback: '«plans».pdf' }),
/option fallback.*ascii/i)
it('should require an ISO-8859-1 string', function () {
assert.throws(contentDisposition.bind(null, '€ rates.pdf', { fallback: '€ rates.pdf' }),
/option fallback.*iso-8859-1/i)
})

it('should use as ASCII fallback', function () {
assert.equal(contentDisposition('«plans».pdf', { fallback: 'plans.pdf' }),
'attachment; filename="plans.pdf"; filename*=UTF-8\'\'%C2%ABplans%C2%BB.pdf')
it('should use as ISO-8859-1 fallback', function () {
assert.equal(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 ASCII', function () {
assert.equal(contentDisposition('"plans".pdf', { fallback: 'plans.pdf' }),
'attachment; filename="plans.pdf"; filename*=UTF-8\'\'%22plans%22.pdf')
it('should use as fallback even when filename is ISO-8859-1', function () {
assert.equal(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 () {
Expand All @@ -129,8 +138,8 @@ describe('contentDisposition(filename, options)', function () {
})

it('should use the basename of the string', function () {
assert.equal(contentDisposition('«plans».pdf', { fallback: '/path/to/plans.pdf' }),
'attachment; filename="plans.pdf"; filename*=UTF-8\'\'%C2%ABplans%C2%BB.pdf')
assert.equal(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 () {
Expand Down

0 comments on commit df2e5df

Please sign in to comment.