diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 76b1021..0000000 --- a/.eslintignore +++ /dev/null @@ -1,3 +0,0 @@ -.nyc_output -coverage -node_modules diff --git a/.eslintrc.yml b/.eslintrc.yml deleted file mode 100644 index 1eece14..0000000 --- a/.eslintrc.yml +++ /dev/null @@ -1,2 +0,0 @@ -root: true -extends: standard diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8beddcf..fcac4c1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,230 +1,28 @@ -name: ci +name: CI on: -- pull_request -- push + - pull_request + - push permissions: contents: read jobs: test: - permissions: - checks: write # for coverallsapp/github-action to create new checks - contents: read # for actions/checkout to fetch code + name: Node.js ${{ matrix.node-version }} runs-on: ubuntu-latest strategy: matrix: - name: - - Node.js 0.8 - - Node.js 0.10 - - Node.js 0.12 - - io.js 1.x - - io.js 2.x - - io.js 3.x - - Node.js 4.x - - Node.js 5.x - - Node.js 6.x - - Node.js 7.x - - Node.js 8.x - - Node.js 9.x - - Node.js 10.x - - Node.js 11.x - - Node.js 12.x - - Node.js 13.x - - Node.js 14.x - - Node.js 15.x - - Node.js 16.x - - Node.js 17.x - - Node.js 18.x - - Node.js 19.x - - Node.js 20.x - - Node.js 21.x - - Node.js 22.x - - include: - - - name: Node.js 0.8 - node-version: "0.8" - npm-i: mocha@2.5.3 - npm-rm: nyc - - - name: Node.js 0.10 - node-version: "0.10" - npm-i: mocha@3.5.3 nyc@10.3.2 - - - name: Node.js 0.12 - node-version: "0.12" - npm-i: mocha@3.5.3 nyc@10.3.2 - - - name: io.js 1.x - node-version: "1.8" - npm-i: mocha@3.5.3 nyc@10.3.2 - - - name: io.js 2.x - node-version: "2.5" - npm-i: mocha@3.5.3 nyc@10.3.2 - - - name: io.js 3.x - node-version: "3.3" - npm-i: mocha@3.5.3 nyc@10.3.2 - - - name: Node.js 4.x - node-version: "4.9" - npm-i: mocha@5.2.0 nyc@11.9.0 - - - name: Node.js 5.x - node-version: "5.12" - npm-i: mocha@5.2.0 nyc@11.9.0 - - - name: Node.js 6.x - node-version: "6.17" - npm-i: mocha@6.2.2 nyc@14.1.1 - - - name: Node.js 7.x - node-version: "7.10" - npm-i: mocha@6.2.2 nyc@14.1.1 - - - name: Node.js 8.x - node-version: "8.17" - npm-i: mocha@7.1.2 nyc@14.1.1 - - - name: Node.js 9.x - node-version: "9.11" - npm-i: mocha@7.1.2 nyc@14.1.1 - - - name: Node.js 10.x - node-version: "10.24" - npm-i: mocha@8.4.0 - - - name: Node.js 11.x - node-version: "11.15" - npm-i: mocha@8.4.0 - - - name: Node.js 12.x - node-version: "12.22" - npm-i: mocha@9.2.2 - - - name: Node.js 13.x - node-version: "13.14" - npm-i: mocha@9.2.2 - - - name: Node.js 14.x - node-version: "14.21" - - - name: Node.js 15.x - node-version: "15.14" - - - name: Node.js 16.x - node-version: "16.20" - - - name: Node.js 17.x - node-version: "17.9" - - - name: Node.js 18.x - node-version: "18.18" - - - name: Node.js 19.x - node-version: "19.9" - - - name: Node.js 20.x - node-version: "20.9" - - - name: Node.js 21.x - node-version: "21.7" - - - name: Node.js 22.x - node-version: "22.0" - - steps: - - uses: actions/checkout@v6.0.0 - - - name: Install Node.js ${{ matrix.node-version }} - shell: bash -eo pipefail -l {0} - run: | - nvm install --default ${{ matrix.node-version }} - if [[ "${{ matrix.node-version }}" == 0.* && "$(cut -d. -f2 <<< "${{ matrix.node-version }}")" -lt 10 ]]; then - nvm install --alias=npm 0.10 - nvm use ${{ matrix.node-version }} - if [[ "$(npm -v)" == 1.1.* ]]; then - nvm exec npm npm install -g npm@1.1 - ln -fs "$(which npm)" "$(dirname "$(nvm which npm)")/npm" - else - sed -i '1s;^.*$;'"$(printf '#!%q' "$(nvm which npm)")"';' "$(readlink -f "$(which npm)")" - fi - npm config set strict-ssl false - fi - dirname "$(nvm which ${{ matrix.node-version }})" >> "$GITHUB_PATH" - - - name: Configure npm - run: | - if [[ "$(npm config get package-lock)" == "true" ]]; then - npm config set package-lock false - else - npm config set shrinkwrap false - fi - - - name: Remove npm module(s) ${{ matrix.npm-rm }} - run: npm rm --silent --save-dev ${{ matrix.npm-rm }} - if: matrix.npm-rm != '' - - - name: Install npm module(s) ${{ matrix.npm-i }} - run: npm install --save-dev ${{ matrix.npm-i }} - if: matrix.npm-i != '' - - - name: Setup Node.js version-specific dependencies - shell: bash - run: | - # eslint for linting - # - remove on Node.js < 8 - if [[ "$(cut -d. -f1 <<< "${{ matrix.node-version }}")" -lt 8 ]]; then - node -pe 'Object.keys(require("./package").devDependencies).join("\n")' | \ - grep -E '^eslint(-|$)' | \ - sort -r | \ - xargs -n1 npm rm --silent --save-dev - fi - - - name: Install Node.js dependencies - run: npm install - - - name: List environment - id: list_env - shell: bash - run: | - echo "node@$(node -v)" - echo "npm@$(npm -v)" - npm -s ls ||: - (npm -s ls --depth=0 ||:) | awk -F'[ @]' 'NR>1 && $2 { print "::set-output name=" $2 "::" $3 }' - - - name: Run tests - shell: bash - run: | - if npm -ps ls nyc | grep -q nyc; then - npm run test-ci - else - npm test - fi - - - name: Lint code - if: steps.list_env.outputs.eslint != '' - run: npm run lint - - - name: Collect code coverage - uses: coverallsapp/github-action@master - if: steps.list_env.outputs.nyc != '' - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - flag-name: run-${{ matrix.test_number }} - parallel: true - - coverage: - permissions: - checks: write # for coverallsapp/github-action to create new checks - needs: test - runs-on: ubuntu-latest + node-version: + - "20" + - "*" steps: - - name: Upload code coverage - uses: coverallsapp/github-action@master - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - parallel-finished: true + - uses: actions/checkout@v5 + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + - run: npm install + - run: npm test + - uses: codecov/codecov-action@v5 + with: + name: Node.js ${{ matrix.node-version }} diff --git a/.gitignore b/.gitignore index ba6c9a8..4c42adf 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ node_modules coverage package-lock.json +dist/ +*.tsbuildinfo diff --git a/HISTORY.md b/HISTORY.md index 70a973d..2b2828f 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,56 +1,45 @@ -1.2.1 / 2019-05-10 -================== +# 1.2.1 / 2019-05-10 - * Improve error when `str` is not a string +- Improve error when `str` is not a string -1.2.0 / 2016-06-01 -================== +# 1.2.0 / 2016-06-01 - * Add `combine` option to combine overlapping ranges +- Add `combine` option to combine overlapping ranges -1.1.0 / 2016-05-13 -================== +# 1.1.0 / 2016-05-13 - * Fix incorrectly returning -1 when there is at least one valid range - * perf: remove internal function +- Fix incorrectly returning -1 when there is at least one valid range +- perf: remove internal function -1.0.3 / 2015-10-29 -================== +# 1.0.3 / 2015-10-29 - * perf: enable strict mode +- perf: enable strict mode -1.0.2 / 2014-09-08 -================== +# 1.0.2 / 2014-09-08 - * Support Node.js 0.6 +- Support Node.js 0.6 -1.0.1 / 2014-09-07 -================== +# 1.0.1 / 2014-09-07 - * Move repository to jshttp +- Move repository to jshttp -1.0.0 / 2013-12-11 -================== +# 1.0.0 / 2013-12-11 - * Add repository to package.json - * Add MIT license +- Add repository to package.json +- Add MIT license -0.0.4 / 2012-06-17 -================== +# 0.0.4 / 2012-06-17 - * Change ret -1 for unsatisfiable and -2 when invalid +- Change ret -1 for unsatisfiable and -2 when invalid -0.0.3 / 2012-06-17 -================== +# 0.0.3 / 2012-06-17 - * Fix last-byte-pos default to len - 1 +- Fix last-byte-pos default to len - 1 -0.0.2 / 2012-06-14 -================== +# 0.0.2 / 2012-06-14 - * Add `.type` +- Add `.type` -0.0.1 / 2012-06-11 -================== +# 0.0.1 / 2012-06-11 - * Initial release +- Initial release diff --git a/README.md b/README.md index 5103c4b..f9528d6 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ $ npm install range-parser ```js -var parseRange = require('range-parser') +var parseRange = require("range-parser"); ``` ### parseRange(size, header, options) @@ -32,21 +32,19 @@ Parse the given `header` string where `size` is the size of the selected representation that is to be partitioned into subranges. An array of subranges will be returned or negative numbers indicating an error parsing. - * `-2` signals a malformed header string - * `-1` signals an unsatisfiable range - - +- `-2` signals a malformed header string +- `-1` signals an unsatisfiable range ```js // parse header from request -var subranges = parseRange(size, req.headers.range) +var subranges = parseRange(size, req.headers.range); // the type of the subranges -if (subranges.type === 'bytes') { +if (subranges.type === "bytes") { // the ranges subranges.forEach(function (r) { // do something with r.start and r.end - }) + }); } ``` @@ -60,10 +58,8 @@ Specifies if overlapping & adjacent subranges should be combined, defaults to `false`. When `true`, ranges will be combined and returned as if they were specified that way in the header. - - ```js -parseRange(100, 'bytes=50-55,0-10,5-10,56-60', { combine: true }) +parseRange(100, "bytes=50-55,0-10,5-10,56-60", { combine: true }); // => [ // { start: 0, end: 10 }, // { start: 50, end: 60 } diff --git a/index.js b/index.js deleted file mode 100644 index 41e1cd3..0000000 --- a/index.js +++ /dev/null @@ -1,183 +0,0 @@ -/*! - * range-parser - * Copyright(c) 2012-2014 TJ Holowaychuk - * Copyright(c) 2015-2016 Douglas Christopher Wilson - * MIT Licensed - */ - -'use strict' - -/** - * Module exports. - * @public - */ - -module.exports = rangeParser - -/** - * Parse "Range" header `str` relative to the given file `size`. - * - * @param {Number} size - * @param {String} str - * @param {Object} [options] - * @return {Array} - * @public - */ - -function rangeParser (size, str, options) { - if (typeof str !== 'string') { - throw new TypeError('argument str must be a string') - } - - var index = str.indexOf('=') - - if (index === -1) { - return -2 - } - - // split the range string - var arr = str.slice(index + 1).split(',') - var ranges = [] - var valid = false - - // add ranges type - ranges.type = str.slice(0, index) - - // parse all ranges - for (var i = 0; i < arr.length; i++) { - var indexOf = arr[i].indexOf('-') - if (indexOf === -1) { - continue - } - - var startStr = arr[i].slice(0, indexOf).trim() - var endStr = arr[i].slice(indexOf + 1).trim() - - var start = parsePos(startStr) - var end = parsePos(endStr) - - if (startStr.length === 0) { - start = size - end - end = size - 1 - } else if (endStr.length === 0) { - end = size - 1 - } - - // limit last-byte-pos to current length - if (end > size - 1) { - end = size - 1 - } - - // invalid format range - if (isNaN(start) || isNaN(end)) { - continue - } - - // skip unsatisfiable ranges - if (start > end || start < 0) { - valid = true - continue - } - - // add range - ranges.push({ - start: start, - end: end - }) - } - - if (ranges.length < 1) { - return valid ? -1 : -2 - } - - return options && options.combine - ? combineRanges(ranges) - : ranges -} - -/** - * Parse string to integer. - * @private - */ - -function parsePos (str) { - if (/^\d+$/.test(str)) return Number(str) - return NaN -} - -/** - * Combine overlapping & adjacent ranges. - * @private - */ - -function combineRanges (ranges) { - var ordered = ranges.map(mapWithIndex).sort(sortByRangeStart) - - for (var j = 0, i = 1; i < ordered.length; i++) { - var range = ordered[i] - var 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 - var combined = ordered.sort(sortByRangeIndex).map(mapWithoutIndex) - - // copy ranges type - combined.type = ranges.type - - return combined -} - -/** - * Map function to add index value to ranges. - * @private - */ - -function mapWithIndex (range, index) { - return { - start: range.start, - end: range.end, - index: 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 -} diff --git a/package.json b/package.json index 4c41fff..0ba444d 100644 --- a/package.json +++ b/package.json @@ -1,48 +1,54 @@ { "name": "range-parser", - "author": "TJ Holowaychuk (http://tjholowaychuk.com)", - "description": "Range header field string parser", "version": "1.2.1", - "contributors": [ - "Douglas Christopher Wilson ", - "James Wyatt Cready ", - "Jonathan Ong (http://jongleberry.com)" - ], - "license": "MIT", + "description": "Range header field string parser", "keywords": [ "range", "parser", - "http" + "http", + "header" ], "repository": "jshttp/range-parser", "funding": { "type": "opencollective", "url": "https://opencollective.com/express" }, - "devDependencies": { - "deep-equal": "1.0.1", - "eslint": "6.0.1", - "eslint-config-standard": "13.0.1", - "eslint-plugin-markdown": "1.0.0", - "eslint-plugin-import": "2.18.0", - "eslint-plugin-node": "9.1.0", - "eslint-plugin-promise": "4.2.1", - "eslint-plugin-standard": "4.0.0", - "mocha": "6.1.4", - "nyc": "14.1.1" - }, + "license": "MIT", + "author": "TJ Holowaychuk (http://tjholowaychuk.com)", + "contributors": [ + "Douglas Christopher Wilson ", + "James Wyatt Cready ", + "Jonathan Ong (http://jongleberry.com)" + ], + "main": "dist/index.js", + "types": "dist/index.d.ts", "files": [ - "HISTORY.md", - "LICENSE", - "index.js" + "dist/" ], - "engines": { - "node": ">= 0.6" - }, "scripts": { - "lint": "eslint --plugin markdown --ext js,md .", - "test": "mocha --reporter spec", - "test-ci": "nyc --reporter=lcovonly --reporter=text npm test", - "test-cov": "nyc --reporter=html --reporter=text npm test" + "bench": "vitest bench", + "build": "ts-scripts build", + "format": "ts-scripts format", + "lint": "ts-scripts lint", + "prepare": "ts-scripts install && npm run build", + "specs": "ts-scripts specs", + "test": "ts-scripts test" + }, + "devDependencies": { + "@borderless/ts-scripts": "^0.15.0", + "@vitest/coverage-v8": "^4.0.18", + "typescript": "^5.9.3", + "vitest": "^4.0.18" + }, + "publishConfig": { + "access": "public" + }, + "ts-scripts": { + "dist": [ + "dist" + ], + "project": [ + "tsconfig.build.json" + ] } } diff --git a/src/index.spec.ts b/src/index.spec.ts new file mode 100644 index 0000000..6da9a82 --- /dev/null +++ b/src/index.spec.ts @@ -0,0 +1,269 @@ +import { describe, it, assert } from "vitest"; +import parse from "./index"; + +describe("parseRange(len, str)", function () { + it("should reject non-string str", function () { + assert.throws( + () => parse(200, {} as any), + TypeError, + /argument str must be a string/, + ); + }); + + it("should return -2 for completely empty header", function () { + assert.strictEqual(parse(200, ""), -2); + }); + + it("should return -2 for range missing dash", function () { + assert.strictEqual(parse(200, "bytes=100200"), -2); + assert.strictEqual(parse(200, "bytes=,100200"), -2); + }); + + it("should return -2 for invalid str", function () { + assert.strictEqual(parse(200, "malformed"), -2); + }); + + it("should return -2 for invalid start byte position", function () { + assert.strictEqual(parse(200, "bytes=x-100"), -2); + }); + + it("should return -2 for invalid end byte position", function () { + assert.strictEqual(parse(200, "bytes=100-x"), -2); + }); + + it("should return -2 for invalid range format", function () { + assert.strictEqual(parse(200, "bytes=--100"), -2); + assert.strictEqual(parse(200, "bytes=100--200"), -2); + assert.strictEqual(parse(200, "bytes=-"), -2); + assert.strictEqual(parse(200, "bytes= - "), -2); + }); + + it("should return -2 for empty range value", function () { + assert.strictEqual(parse(200, "bytes="), -2); + assert.strictEqual(parse(200, "bytes=,"), -2); + assert.strictEqual(parse(200, "bytes= , , "), -2); + }); + + it("should return -2 with multiple dashes in range", function () { + assert.strictEqual(parse(200, "bytes=100-200-300"), -2); + }); + + it("should return -2 for negative start byte position", function () { + assert.strictEqual(parse(200, "bytes=-100-150"), -2); + }); + + it("should return -2 for invalid number format", function () { + assert.strictEqual(parse(200, "bytes=01a-150"), -2); + assert.strictEqual(parse(200, "bytes=100-15b0"), -2); + }); + + it("should return -2 when all multiple ranges have invalid format", function () { + assert.strictEqual(parse(200, "bytes=y-v,x-"), -2); + assert.strictEqual(parse(200, "bytes=abc-def,ghi-jkl"), -2); + assert.strictEqual(parse(200, "bytes=x-,y-,z-"), -2); + }); + + it("should return -1 for unsatisfiable range", function () { + assert.strictEqual(parse(200, "bytes=500-600"), -1); + }); + + it("should return -1 for unsatisfiable range with multiple ranges", function () { + assert.strictEqual(parse(200, "bytes=500-600,601-700"), -1); + }); + + it("should return -1 if all specified ranges are invalid", function () { + assert.strictEqual(parse(200, "bytes=500-20"), -1); + assert.strictEqual(parse(200, "bytes=500-999"), -1); + assert.strictEqual(parse(200, "bytes=500-999,1000-1499"), -1); + }); + + it("should return -1 for mixed invalid and unsatisfiable ranges", function () { + assert.strictEqual(parse(200, "bytes=abc-def,500-999"), -1); + assert.strictEqual(parse(200, "bytes=500-999,xyz-uvw"), -1); + assert.strictEqual(parse(200, "bytes=abc-def,500-999,xyz-uvw"), -1); + }); + + it("should parse str", function () { + var range = parse(1000, "bytes=0-499"); + assert.deepEqual( + range, + Object.assign([{ start: 0, end: 499 }], { type: "bytes" }), + ); + }); + + it("should cap end at size", function () { + var range = parse(200, "bytes=0-499"); + assert.deepEqual( + range, + Object.assign([{ start: 0, end: 199 }], { type: "bytes" }), + ); + }); + + it("should parse str", function () { + var range = parse(1000, "bytes=40-80"); + assert.deepEqual( + range, + Object.assign([{ start: 40, end: 80 }], { type: "bytes" }), + ); + }); + + it("should parse str asking for last n bytes", function () { + var range = parse(1000, "bytes=-400"); + assert.deepEqual( + range, + Object.assign([{ start: 600, end: 999 }], { type: "bytes" }), + ); + }); + + it("should parse str with only start", function () { + var range = parse(1000, "bytes=400-"); + assert.deepEqual( + range, + Object.assign([{ start: 400, end: 999 }], { type: "bytes" }), + ); + }); + + it('should parse "bytes=0-"', function () { + var range = parse(1000, "bytes=0-"); + assert.deepEqual( + range, + Object.assign([{ start: 0, end: 999 }], { type: "bytes" }), + ); + }); + + it("should parse str with no bytes", function () { + var range = parse(1000, "bytes=0-0"); + assert.deepEqual( + range, + Object.assign([{ start: 0, end: 0 }], { type: "bytes" }), + ); + }); + + it("should parse str asking for last byte", function () { + var range = parse(1000, "bytes=-1"); + assert.deepEqual( + range, + Object.assign([{ start: 999, end: 999 }], { type: "bytes" }), + ); + }); + + it("should ignore invalid format range when valid range exists", function () { + var range = parse(1000, "bytes=100-200,x-"); + assert.deepEqual( + range, + Object.assign([{ start: 100, end: 200 }], { type: "bytes" }), + ); + }); + + it("should ignore invalid format ranges when some are valid", function () { + var range = parse(1000, "bytes=x-,0-100,y-"); + assert.deepEqual( + range, + Object.assign([{ start: 0, end: 100 }], { type: "bytes" }), + ); + }); + + it("should ignore invalid format ranges at different positions", function () { + var range = parse(1000, "bytes=0-50,abc-def,100-150"); + assert.deepEqual( + range, + Object.assign( + [ + { start: 0, end: 50 }, + { start: 100, end: 150 }, + ], + { type: "bytes" }, + ), + ); + }); + + it("should parse str with multiple ranges", function () { + var range = parse(1000, "bytes=40-80,81-90,-1"); + assert.deepEqual( + range, + Object.assign( + [ + { start: 40, end: 80 }, + { start: 81, end: 90 }, + { start: 999, end: 999 }, + ], + { type: "bytes" }, + ), + ); + }); + + it("should parse str with some invalid ranges", function () { + var range = parse(200, "bytes=0-499,1000-,500-999"); + assert.deepEqual( + range, + Object.assign([{ start: 0, end: 199 }], { type: "bytes" }), + ); + }); + + it("should parse str with whitespace", function () { + var range = parse(1000, "bytes= 40-80 , 81-90 , -1 "); + assert.deepEqual( + range, + Object.assign( + [ + { start: 40, end: 80 }, + { start: 81, end: 90 }, + { start: 999, end: 999 }, + ], + { type: "bytes" }, + ), + ); + }); + + it("should parse non-byte range", function () { + var range = parse(1000, "items=0-5"); + assert.deepEqual( + range, + Object.assign([{ start: 0, end: 5 }], { type: "items" }), + ); + }); + + describe("when combine: true", function () { + it("should combine overlapping ranges", function () { + var range = parse(150, "bytes=0-4,90-99,5-75,100-199,101-102", { + combine: true, + }); + + assert.deepEqual( + range, + Object.assign( + [ + { start: 0, end: 75 }, + { start: 90, end: 149 }, + ], + { type: "bytes" }, + ), + ); + }); + + it("should retain original order", function () { + var range = parse(150, "bytes=-1,20-100,0-1,101-120", { combine: true }); + + assert.deepEqual( + range, + Object.assign( + [ + { start: 149, end: 149 }, + { start: 20, end: 120 }, + { start: 0, end: 1 }, + ], + { type: "bytes" }, + ), + ); + }); + }); + + it("should ignore whitespace-only invalid ranges when valid present", function () { + var range = parse(1000, "bytes= , 0-10"); + + assert.deepEqual( + range, + Object.assign([{ start: 0, end: 10 }], { type: "bytes" }), + ); + }); +}); diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..88f6f9b --- /dev/null +++ b/src/index.ts @@ -0,0 +1,199 @@ +/*! + * range-parser + * Copyright(c) 2012-2014 TJ Holowaychuk + * Copyright(c) 2015-2016 Douglas Christopher Wilson + * MIT Licensed + */ + +"use strict"; + +/** + * Module exports. + * @public + */ + +export = rangeParser; + +namespace rangeParser { + export interface Range { + start: number; + end: number; + } + + export interface Result extends Array { + type: string; + } + + export interface Options { + combine?: boolean; + } +} + +/** + * Parse "Range" header `str` relative to the given file `size`. + * + * @param {Number} size + * @param {String} str + * @param {Object} [options] + * @return {Array} + * @public + */ + +function rangeParser(size: number, str: string, options?: rangeParser.Options) { + if (typeof str !== "string") { + throw new TypeError("argument str must be a string"); + } + + var index = str.indexOf("="); + + if (index === -1) { + return -2; + } + + // split the range string + var valid = false; + var arr = str.slice(index + 1).split(","); + var ranges: rangeParser.Result = Object.assign([], { + type: str.slice(0, index), + }); + + // parse all ranges + for (var i = 0; i < arr.length; i++) { + var indexOf = arr[i].indexOf("-"); + if (indexOf === -1) { + continue; + } + + var startStr = arr[i].slice(0, indexOf).trim(); + var endStr = arr[i].slice(indexOf + 1).trim(); + + var start = parsePos(startStr); + var end = parsePos(endStr); + + if (startStr.length === 0) { + start = size - end; + end = size - 1; + } else if (endStr.length === 0) { + end = size - 1; + } + + // limit last-byte-pos to current length + if (end > size - 1) { + end = size - 1; + } + + // invalid format range + if (isNaN(start) || isNaN(end)) { + continue; + } + + // skip unsatisfiable ranges + if (start > end || start < 0) { + valid = true; + continue; + } + + // add range + ranges.push({ + start: start, + end: end, + }); + } + + if (ranges.length < 1) { + return valid ? -1 : -2; + } + + return options && options.combine ? combineRanges(ranges) : ranges; +} + +/** + * Parse string to integer. + * @private + */ + +function parsePos(str: string) { + if (/^\d+$/.test(str)) return Number(str); + return NaN; +} + +/** + * Combine overlapping & adjacent ranges. + * @private + */ + +function combineRanges(ranges: rangeParser.Result): rangeParser.Result { + var ordered = ranges.map(mapWithIndex).sort(sortByRangeStart); + + for (var j = 0, i = 1; i < ordered.length; i++) { + var range = ordered[i]; + var 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 + var combined = Object.assign( + ordered.sort(sortByRangeIndex).map(mapWithoutIndex), + { type: ranges.type }, + ); + + return combined; +} + +interface RangeWithIndex extends rangeParser.Range { + index: number; +} + +/** + * Map function to add index value to ranges. + * @private + */ + +function mapWithIndex(range: rangeParser.Range, index: number): RangeWithIndex { + return { + start: range.start, + end: range.end, + index: index, + }; +} + +/** + * Map function to remove index value from ranges. + * @private + */ + +function mapWithoutIndex(range: rangeParser.Range): rangeParser.Range { + return { + start: range.start, + end: range.end, + }; +} + +/** + * Sort function to sort ranges by index. + * @private + */ + +function sortByRangeIndex(a: RangeWithIndex, b: RangeWithIndex) { + return a.index - b.index; +} + +/** + * Sort function to sort ranges by start position. + * @private + */ + +function sortByRangeStart(a: rangeParser.Range, b: rangeParser.Range) { + return a.start - b.start; +} diff --git a/test/.eslintrc.yml b/test/.eslintrc.yml deleted file mode 100644 index 9808c3b..0000000 --- a/test/.eslintrc.yml +++ /dev/null @@ -1,2 +0,0 @@ -env: - mocha: true diff --git a/test/range-parser.js b/test/range-parser.js deleted file mode 100644 index 9f2c483..0000000 --- a/test/range-parser.js +++ /dev/null @@ -1,219 +0,0 @@ -var assert = require('assert') -var deepEqual = require('deep-equal') -var parse = require('..') - -describe('parseRange(len, str)', function () { - it('should reject non-string str', function () { - assert.throws(parse.bind(null, 200, {}), - /TypeError: argument str must be a string/) - }) - - it('should return -2 for completely empty header', function () { - assert.strictEqual(parse(200, ''), -2) - }) - - it('should return -2 for range missing dash', function () { - assert.strictEqual(parse(200, 'bytes=100200'), -2) - assert.strictEqual(parse(200, 'bytes=,100200'), -2) - }) - - it('should return -2 for invalid str', function () { - assert.strictEqual(parse(200, 'malformed'), -2) - }) - - it('should return -2 for invalid start byte position', function () { - assert.strictEqual(parse(200, 'bytes=x-100'), -2) - }) - - it('should return -2 for invalid end byte position', function () { - assert.strictEqual(parse(200, 'bytes=100-x'), -2) - }) - - it('should return -2 for invalid range format', function () { - assert.strictEqual(parse(200, 'bytes=--100'), -2) - assert.strictEqual(parse(200, 'bytes=100--200'), -2) - assert.strictEqual(parse(200, 'bytes=-'), -2) - assert.strictEqual(parse(200, 'bytes= - '), -2) - }) - - it('should return -2 for empty range value', function () { - assert.strictEqual(parse(200, 'bytes='), -2) - assert.strictEqual(parse(200, 'bytes=,'), -2) - assert.strictEqual(parse(200, 'bytes= , , '), -2) - }) - - it('should return -2 with multiple dashes in range', function () { - assert.strictEqual(parse(200, 'bytes=100-200-300'), -2) - }) - - it('should return -2 for negative start byte position', function () { - assert.strictEqual(parse(200, 'bytes=-100-150'), -2) - }) - - it('should return -2 for invalid number format', function () { - assert.strictEqual(parse(200, 'bytes=01a-150'), -2) - assert.strictEqual(parse(200, 'bytes=100-15b0'), -2) - }) - - it('should return -2 when all multiple ranges have invalid format', function () { - assert.strictEqual(parse(200, 'bytes=y-v,x-'), -2) - assert.strictEqual(parse(200, 'bytes=abc-def,ghi-jkl'), -2) - assert.strictEqual(parse(200, 'bytes=x-,y-,z-'), -2) - }) - - it('should return -1 for unsatisfiable range', function () { - assert.strictEqual(parse(200, 'bytes=500-600'), -1) - }) - - it('should return -1 for unsatisfiable range with multiple ranges', function () { - assert.strictEqual(parse(200, 'bytes=500-600,601-700'), -1) - }) - - it('should return -1 if all specified ranges are invalid', function () { - assert.strictEqual(parse(200, 'bytes=500-20'), -1) - assert.strictEqual(parse(200, 'bytes=500-999'), -1) - assert.strictEqual(parse(200, 'bytes=500-999,1000-1499'), -1) - }) - - it('should return -1 for mixed invalid and unsatisfiable ranges', function () { - assert.strictEqual(parse(200, 'bytes=abc-def,500-999'), -1) - assert.strictEqual(parse(200, 'bytes=500-999,xyz-uvw'), -1) - assert.strictEqual(parse(200, 'bytes=abc-def,500-999,xyz-uvw'), -1) - }) - - it('should parse str', function () { - var range = parse(1000, 'bytes=0-499') - assert.strictEqual(range.type, 'bytes') - assert.strictEqual(range.length, 1) - deepEqual(range[0], { start: 0, end: 499 }) - }) - - it('should cap end at size', function () { - var range = parse(200, 'bytes=0-499') - assert.strictEqual(range.type, 'bytes') - assert.strictEqual(range.length, 1) - deepEqual(range[0], { start: 0, end: 199 }) - }) - - it('should parse str', function () { - var range = parse(1000, 'bytes=40-80') - assert.strictEqual(range.type, 'bytes') - assert.strictEqual(range.length, 1) - deepEqual(range[0], { start: 40, end: 80 }) - }) - - it('should parse str asking for last n bytes', function () { - var range = parse(1000, 'bytes=-400') - assert.strictEqual(range.type, 'bytes') - assert.strictEqual(range.length, 1) - deepEqual(range[0], { start: 600, end: 999 }) - }) - - it('should parse str with only start', function () { - var range = parse(1000, 'bytes=400-') - assert.strictEqual(range.type, 'bytes') - assert.strictEqual(range.length, 1) - deepEqual(range[0], { start: 400, end: 999 }) - }) - - it('should parse "bytes=0-"', function () { - var range = parse(1000, 'bytes=0-') - assert.strictEqual(range.type, 'bytes') - assert.strictEqual(range.length, 1) - deepEqual(range[0], { start: 0, end: 999 }) - }) - - it('should parse str with no bytes', function () { - var range = parse(1000, 'bytes=0-0') - assert.strictEqual(range.type, 'bytes') - assert.strictEqual(range.length, 1) - deepEqual(range[0], { start: 0, end: 0 }) - }) - - it('should parse str asking for last byte', function () { - var range = parse(1000, 'bytes=-1') - assert.strictEqual(range.type, 'bytes') - assert.strictEqual(range.length, 1) - deepEqual(range[0], { start: 999, end: 999 }) - }) - - it('should ignore invalid format range when valid range exists', function () { - var range = parse(1000, 'bytes=100-200,x-') - assert.strictEqual(range.type, 'bytes') - assert.strictEqual(range.length, 1) - deepEqual(range[0], { start: 100, end: 200 }) - }) - - it('should ignore invalid format ranges when some are valid', function () { - var range = parse(1000, 'bytes=x-,0-100,y-') - assert.strictEqual(range.type, 'bytes') - assert.strictEqual(range.length, 1) - deepEqual(range[0], { start: 0, end: 100 }) - }) - - it('should ignore invalid format ranges at different positions', function () { - var range = parse(1000, 'bytes=0-50,abc-def,100-150') - assert.strictEqual(range.type, 'bytes') - assert.strictEqual(range.length, 2) - deepEqual(range[0], { start: 0, end: 50 }) - deepEqual(range[1], { start: 100, end: 150 }) - }) - - it('should parse str with multiple ranges', function () { - var range = parse(1000, 'bytes=40-80,81-90,-1') - assert.strictEqual(range.type, 'bytes') - assert.strictEqual(range.length, 3) - deepEqual(range[0], { start: 40, end: 80 }) - deepEqual(range[1], { start: 81, end: 90 }) - deepEqual(range[2], { start: 999, end: 999 }) - }) - - it('should parse str with some invalid ranges', function () { - var range = parse(200, 'bytes=0-499,1000-,500-999') - assert.strictEqual(range.type, 'bytes') - assert.strictEqual(range.length, 1) - deepEqual(range[0], { start: 0, end: 199 }) - }) - - it('should parse str with whitespace', function () { - var range = parse(1000, 'bytes= 40-80 , 81-90 , -1 ') - assert.strictEqual(range.type, 'bytes') - assert.strictEqual(range.length, 3) - deepEqual(range[0], { start: 40, end: 80 }) - deepEqual(range[1], { start: 81, end: 90 }) - deepEqual(range[2], { start: 999, end: 999 }) - }) - - it('should parse non-byte range', function () { - var range = parse(1000, 'items=0-5') - assert.strictEqual(range.type, 'items') - assert.strictEqual(range.length, 1) - deepEqual(range[0], { start: 0, end: 5 }) - }) - - describe('when combine: true', function () { - it('should combine overlapping ranges', function () { - var range = parse(150, 'bytes=0-4,90-99,5-75,100-199,101-102', { combine: true }) - assert.strictEqual(range.type, 'bytes') - assert.strictEqual(range.length, 2) - deepEqual(range[0], { start: 0, end: 75 }) - deepEqual(range[1], { start: 90, end: 149 }) - }) - - it('should retain original order', function () { - var range = parse(150, 'bytes=-1,20-100,0-1,101-120', { combine: true }) - assert.strictEqual(range.type, 'bytes') - assert.strictEqual(range.length, 3) - deepEqual(range[0], { start: 149, end: 149 }) - deepEqual(range[1], { start: 20, end: 120 }) - deepEqual(range[2], { start: 0, end: 1 }) - }) - }) - - it('should ignore whitespace-only invalid ranges when valid present', function () { - var range = parse(1000, 'bytes= , 0-10') - assert.strictEqual(range.type, 'bytes') - assert.strictEqual(range.length, 1) - deepEqual(range[0], { start: 0, end: 10 }) - }) -}) diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..3db8e88 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "types": [] + }, + "exclude": ["src/**/*.spec.ts", "src/**/*.bench.ts"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1d2dfa8 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "@borderless/ts-scripts/configs/tsconfig.json", + "compilerOptions": { + "target": "ES2015", + "lib": ["ES2015"], + "rootDir": "src", + "outDir": "dist", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "types": [] + }, + "include": ["src/**/*"] +}