Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

integrate range-parser #30

Merged
merged 2 commits into from
Jan 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions benchmarks/parseRange.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
'use strict'

const benchmark = require('benchmark')
const parseRange = require('../lib/parseRange')

const size = 150
const range = 'bytes=0-4,90-99,5-75,100-199,101-102'

new benchmark.Suite()
.add('parseRange', function () { parseRange(size, range) }, { minSamples: 100 })
.on('cycle', function onCycle (event) { console.log(String(event.target)) })
.run({ async: false })
10 changes: 4 additions & 6 deletions lib/SendStream.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ const fresh = require('fresh')
const mime = require('mime')
const ms = require('ms')
const onFinished = require('on-finished')
const parseRange = require('range-parser')

const { clearHeaders } = require('./clearHeaders')
const { collapseLeadingSlashes } = require('./collapseLeadingSlashes')
Expand All @@ -30,6 +29,7 @@ const { createHttpError } = require('./createHttpError')
const { isUtf8MimeType } = require('./isUtf8MimeType')
const { normalizeList } = require('./normalizeList')
const { parseHttpDate } = require('./parseHttpDate')
const { parseRange } = require('./parseRange')
const { parseTokenList } = require('./parseTokenList')
const { setHeaders } = require('./setHeaders')

Expand Down Expand Up @@ -547,15 +547,13 @@ SendStream.prototype.send = function send (path, stat) {

// Range support
if (this._acceptRanges && ranges !== undefined && BYTES_RANGE_REGEXP.test(ranges)) {
// parse
ranges = parseRange(len, ranges, {
combine: true
})

// If-Range support
if (!this.isRangeFresh()) {
debug('range stale')
ranges = -2
} else {
// parse
ranges = parseRange(len, ranges)
}

// unsatisfiable
Expand Down
135 changes: 135 additions & 0 deletions lib/parseRange.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
'use strict'

/*!
* Based on range-parser
*
* Copyright(c) 2012-2014 TJ Holowaychuk
* Copyright(c) 2015-2016 Douglas Christopher Wilson
* MIT Licensed
*/

/**
* Parse "Range" header `str` relative to the given file `size`.
*
* @param {Number} size
* @param {String} str
* @return {Array}
* @public
*/

module.exports.parseRange = function parseRange (size, str) {
const index = str.indexOf('=')

// split the range string
const arr = str.slice(index + 1).split(',')
const ranges = []

// parse all ranges
for (let i = 0; i < arr.length; i++) {
const range = arr[i].split('-')
let start = parseInt(range[0], 10)
let end = parseInt(range[1], 10)

// -nnn
if (isNaN(start)) {
start = size - end
end = size - 1
// nnn-
} else if (isNaN(end)) {
end = size - 1
}

// limit last-byte-pos to current length
if (end > size - 1) {
end = size - 1
}

// invalid or unsatisfiable
if (isNaN(start) || isNaN(end) || start > end || start < 0) {
continue
}

// add range
ranges.push({
start,
end
})
}

if (ranges.length === 0) {
// unsatisifiable
return -1
}

const ordered = ranges.map(mapWithIndex).sort(sortByRangeStart)

let j = 0
const il = ordered.length
for (let i = 1; i < il; ++i) {
const range = ordered[i]
const current = ordered[j]

if (range.start > current.end + 1) {
// next range
ordered[++j] = range
} else if (range.end > current.end) {
// extend range
current.end = range.end
current.index = Math.min(current.index, range.index)
}
}

// trim ordered array
ordered.length = j + 1

// generate combined range
const combined = ordered.sort(sortByRangeIndex).map(mapWithoutIndex)

// copy ranges type
combined.type = str.slice(0, index)

return combined
}

/**
* Map function to add index value to ranges.
* @private
*/

function mapWithIndex (range, index) {
return {
start: range.start,
end: range.end,
index
}
}

/**
* Map function to remove index value from ranges.
* @private
*/

function mapWithoutIndex (range) {
return {
start: range.start,
end: range.end
}
}

/**
* Sort function to sort ranges by index.
* @private
*/

function sortByRangeIndex (a, b) {
return a.index - b.index
}

/**
* Sort function to sort ranges by start position.
* @private
*/

function sortByRangeStart (a, b) {
return a.start - b.start
}
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@
"server"
],
"dependencies": {
"benchmark": "^2.1.4",
"escape-html": "~1.0.3",
"fast-decode-uri-component": "^1.0.1",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"mime": "^3.0.0",
"ms": "2.1.3",
"on-finished": "2.4.1",
"range-parser": "~1.2.1"
"on-finished": "2.4.1"
},
"devDependencies": {
"@fastify/pre-commit": "^2.0.2",
Expand Down
114 changes: 114 additions & 0 deletions test/parseRange.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
'use strict'

const { test } = require('tap')
const { parseRange } = require('../lib/parseRange')

test('parseRange', function (t) {
t.plan(13)

t.test('should return -1 if all specified ranges are invalid', function (t) {
t.plan(3)
t.equal(parseRange(200, 'bytes=500-20'), -1)
t.equal(parseRange(200, 'bytes=500-999'), -1)
t.equal(parseRange(200, 'bytes=500-999,1000-1499'), -1)
})

t.test('should parse str', function (t) {
t.plan(3)
const range = parseRange(1000, 'bytes=0-499')
t.equal(range.type, 'bytes')
t.equal(range.length, 1)
t.strictSame(range[0], { start: 0, end: 499 })
})

t.test('should cap end at size', function (t) {
t.plan(3)
const range = parseRange(200, 'bytes=0-499')
t.equal(range.type, 'bytes')
t.equal(range.length, 1)
t.strictSame(range[0], { start: 0, end: 199 })
})

t.test('should parse str', function (t) {
t.plan(3)
const range = parseRange(1000, 'bytes=40-80')
t.equal(range.type, 'bytes')
t.equal(range.length, 1)
t.strictSame(range[0], { start: 40, end: 80 })
})

t.test('should parse str asking for last n bytes', function (t) {
t.plan(3)
const range = parseRange(1000, 'bytes=-400')
t.equal(range.type, 'bytes')
t.equal(range.length, 1)
t.strictSame(range[0], { start: 600, end: 999 })
})

t.test('should parse str with only start', function (t) {
t.plan(3)
const range = parseRange(1000, 'bytes=400-')
t.equal(range.type, 'bytes')
t.equal(range.length, 1)
t.strictSame(range[0], { start: 400, end: 999 })
})

t.test('should parse "bytes=0-"', function (t) {
t.plan(3)
const range = parseRange(1000, 'bytes=0-')
t.equal(range.type, 'bytes')
t.equal(range.length, 1)
t.strictSame(range[0], { start: 0, end: 999 })
})

t.test('should parse str with no bytes', function (t) {
t.plan(3)
const range = parseRange(1000, 'bytes=0-0')
t.equal(range.type, 'bytes')
t.equal(range.length, 1)
t.strictSame(range[0], { start: 0, end: 0 })
})

t.test('should parse str asking for last byte', function (t) {
t.plan(3)
const range = parseRange(1000, 'bytes=-1')
t.equal(range.type, 'bytes')
t.equal(range.length, 1)
t.strictSame(range[0], { start: 999, end: 999 })
})

t.test('should parse str with some invalid ranges', function (t) {
t.plan(3)
const range = parseRange(200, 'bytes=0-499,1000-,500-999')
t.equal(range.type, 'bytes')
t.equal(range.length, 1)
t.strictSame(range[0], { start: 0, end: 199 })
})

t.test('should parse non-byte range', function (t) {
t.plan(3)
const range = parseRange(1000, 'items=0-5')
t.equal(range.type, 'items')
t.equal(range.length, 1)
t.strictSame(range[0], { start: 0, end: 5 })
})

t.test('should combine overlapping ranges', function (t) {
t.plan(4)
const range = parseRange(150, 'bytes=0-4,90-99,5-75,100-199,101-102', { combine: true })
t.equal(range.type, 'bytes')
t.equal(range.length, 2)
t.strictSame(range[0], { start: 0, end: 75 })
t.strictSame(range[1], { start: 90, end: 149 })
})

t.test('should retain original order', function (t) {
t.plan(5)
const range = parseRange(150, 'bytes=-1,20-100,0-1,101-120', { combine: true })
t.equal(range.type, 'bytes')
t.equal(range.length, 3)
t.strictSame(range[0], { start: 149, end: 149 })
t.strictSame(range[1], { start: 20, end: 120 })
t.strictSame(range[2], { start: 0, end: 1 })
})
})