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..3c35fb2 100644 --- a/README.md +++ b/README.md @@ -10,60 +10,45 @@ Range header field parser. ## Installation -This is a [Node.js](https://nodejs.org/en/) module available through the -[npm registry](https://www.npmjs.com/). Installation is done using the -[`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally): - ```sh $ npm install range-parser ``` ## API - - ```js -var parseRange = require('range-parser') +import { parse, combineRanges } from "range-parser"; ``` -### parseRange(size, header, options) +### `parse(size, header)` 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 +representation that is to be partitioned into subranges. A type and 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 ```js // parse header from request -var subranges = parseRange(size, req.headers.range) +var subranges = parse(size, req.headers.range); // the type of the subranges -if (subranges.type === 'bytes') { +if (subranges.type === "bytes") { // the ranges - subranges.forEach(function (r) { + subranges.ranges.forEach(function (r) { // do something with r.start and r.end - }) + }); } ``` -#### Options - -These properties are accepted in the options object. - -##### combine - -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. +### `combineRanges(ranges)` - +Overlapping & adjacent ranges will be combined and returned. ```js -parseRange(100, 'bytes=50-55,0-10,5-10,56-60', { combine: true }) +const { ranges } = parseRange(100, "bytes=50-55,0-10,5-10,56-60"); +combineRanges(ranges); // => [ // { start: 0, end: 10 }, // { start: 50, end: 60 } diff --git a/index.js b/index.js deleted file mode 100644 index 1d09e26..0000000 --- a/index.js +++ /dev/null @@ -1,181 +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 = [] - - // 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) { - return -2 - } - - 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 - } - - if (isNaN(start) || isNaN(end)) { - return -2 - } - - // invalid or unsatisifiable - if (start > end || start < 0) { - continue - } - - // add range - ranges.push({ - start: start, - end: end - }) - } - - if (ranges.length < 1) { - // unsatisifiable - return -1 - } - - 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..ff6a82a 100644 --- a/package.json +++ b/package.json @@ -1,48 +1,52 @@ { "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)" + ], "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.bench.ts b/src/index.bench.ts new file mode 100644 index 0000000..e37e00c --- /dev/null +++ b/src/index.bench.ts @@ -0,0 +1,34 @@ +import { describe, bench } from "vitest"; +import { parse, combineRanges } from "./index"; + +describe("parse", () => { + bench("1000 byte range", () => { + parse(1000, "bytes=0-499"); + }); + + bench("multiple byte ranges", () => { + parse(1000, "bytes=0-99,200-299,400-499,600-699,800-899"); + }); +}); + +describe("combineRanges", () => { + bench("simple overlapping ranges", () => { + combineRanges([ + { + start: 0, + end: 499, + }, + { start: 400, end: 999 }, + ]); + }); + + bench("complex overlapping ranges", () => { + combineRanges([ + { start: 0, end: 199 }, + { start: 150, end: 349 }, + { start: 300, end: 499 }, + { start: 400, end: 699 }, + { start: 600, end: 999 }, + ]); + }); +}); diff --git a/src/index.spec.ts b/src/index.spec.ts new file mode 100644 index 0000000..62eef0d --- /dev/null +++ b/src/index.spec.ts @@ -0,0 +1,189 @@ +import { describe, it, assert } from "vitest"; +import { parse, combineRanges } from "./index"; + +describe("parse(len, str)", function () { + it("should return -2 for invalid str", function () { + assert.strictEqual(parse(200, "malformed"), -2); + }); + + it("should return -2 for missing bytes", function () { + assert.strictEqual(parse(200, "bytes="), -2); + assert.strictEqual(parse(200, "bytes= "), -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 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 -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 parse str", function () { + var range = parse(1000, "bytes=0-499"); + assert.deepEqual(range, { + type: "bytes", + ranges: [{ start: 0, end: 499 }], + }); + }); + + it("should cap end at size", function () { + var range = parse(200, "bytes=0-499"); + assert.deepEqual(range, { + type: "bytes", + ranges: [{ start: 0, end: 199 }], + }); + }); + + it("should parse str", function () { + var range = parse(1000, "bytes=40-80"); + assert.deepEqual(range, { + type: "bytes", + ranges: [{ start: 40, end: 80 }], + }); + }); + + it("should parse str asking for last n bytes", function () { + var range = parse(1000, "bytes=-400"); + assert.deepEqual(range, { + type: "bytes", + ranges: [{ start: 600, end: 999 }], + }); + }); + + it("should parse str with only start", function () { + var range = parse(1000, "bytes=400-"); + assert.deepEqual(range, { + type: "bytes", + ranges: [{ start: 400, end: 999 }], + }); + }); + + it('should parse "bytes=0-"', function () { + var range = parse(1000, "bytes=0-"); + assert.deepEqual(range, { + type: "bytes", + ranges: [{ start: 0, end: 999 }], + }); + }); + + it("should parse str with no bytes", function () { + var range = parse(1000, "bytes=0-0"); + assert.deepEqual(range, { + type: "bytes", + ranges: [{ start: 0, end: 0 }], + }); + }); + + it("should parse str asking for last byte", function () { + var range = parse(1000, "bytes=-1"); + assert.deepEqual(range, { + type: "bytes", + ranges: [{ start: 999, end: 999 }], + }); + }); + + it("should parse str with multiple ranges", function () { + var range = parse(1000, "bytes=40-80,81-90,-1"); + assert.deepEqual(range, { + type: "bytes", + ranges: [ + { start: 40, end: 80 }, + { start: 81, end: 90 }, + { start: 999, end: 999 }, + ], + }); + }); + + it("should parse str with some invalid ranges", function () { + var range = parse(200, "bytes=0-499,1000-,500-999"); + assert.deepEqual(range, { + type: "bytes", + ranges: [{ start: 0, end: 199 }], + }); + }); + + it("should parse str with whitespace", function () { + var range = parse(1000, "bytes= 40-80 , 81-90 , -1 "); + assert.deepEqual(range, { + type: "bytes", + ranges: [ + { start: 40, end: 80 }, + { start: 81, end: 90 }, + { start: 999, end: 999 }, + ], + }); + }); + + it("should parse non-byte range", function () { + var range = parse(1000, "items=0-5"); + assert.deepEqual(range, { + type: "items", + ranges: [{ start: 0, end: 5 }], + }); + }); +}); + +describe("combineRanges(ranges)", function () { + it("should combine overlapping ranges", function () { + var combined = combineRanges([ + { start: 0, end: 4 }, + { start: 90, end: 99 }, + { start: 5, end: 75 }, + { start: 100, end: 199 }, + { start: 101, end: 102 }, + ]); + assert.deepEqual(combined, [ + { start: 0, end: 75 }, + { start: 90, end: 199 }, + ]); + }); + + it("should retain original order", function () { + var combined = combineRanges([ + { start: 149, end: 149 }, + { start: 20, end: 100 }, + { start: 0, end: 1 }, + { start: 101, end: 120 }, + ]); + assert.deepEqual(combined, [ + { start: 149, end: 149 }, + { start: 20, end: 120 }, + { start: 0, end: 1 }, + ]); + }); +}); diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..4861bc8 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,182 @@ +/*! + * range-parser + * Copyright(c) 2012-2014 TJ Holowaychuk + * Copyright(c) 2015-2016 Douglas Christopher Wilson + * MIT Licensed + */ + +/** + * Range object contains normalized start and end positions. + */ +export interface Range { + start: number; + end: number; +} + +/** + * Result object contains type and array of Range objects. + */ +export interface Result { + type: string; + ranges: Range[]; +} + +/** + * Parse "Range" header `str` relative to the given file `size`. + * + * @param {Number} size + * @param {String} str + * @param {Object} [options] + * @return {Array} + * @public + */ +export function parse(size: number, str: string): Result | -1 | -2 { + const eqIndex = str.indexOf("="); + if (eqIndex === -1) return -2; + + const ranges: Range[] = []; + const type = str.slice(0, eqIndex); + const len = str.length; + let index = eqIndex + 1; + + do { + const commaIndex = str.indexOf(",", index); + const endIndex = commaIndex !== -1 ? commaIndex : len; + const dashIndex = str.indexOf("-", index); + + if (dashIndex === -1 || dashIndex > endIndex) { + return -2; + } + + // handle ows around numbers + let startStrIndex = index; + let endStrIndex = endIndex; + while (str.charCodeAt(startStrIndex) === 32) startStrIndex++; + while (str.charCodeAt(endStrIndex - 1) === 32) endStrIndex--; + + let start = NaN; + let end = NaN; + + if (startStrIndex === dashIndex) { + if (endStrIndex === dashIndex + 1) return -2; // just "-" is not valid + + // suffix-byte-range-spec, e.g. "-500" + start = size - parsePos(str, dashIndex + 1, endStrIndex); + end = size - 1; + } else { + start = parsePos(str, startStrIndex, dashIndex); + if (endStrIndex === dashIndex + 1) { + // open-ended range, e.g. "9500-" + end = size - 1; + } else { + end = parsePos(str, dashIndex + 1, endStrIndex); + if (end > size - 1) end = size - 1; + } + } + + index = endIndex + 1; + + if (isNaN(start) || isNaN(end)) { + return -2; + } + + // invalid or unsatisfiable + if (start > end || start < 0) { + continue; + } + + ranges.push({ + start: start, + end: end, + }); + } while (index < len); + + if (ranges.length < 1) { + // unsatisfiable + return -1; + } + + return { type, ranges }; +} + +/** + * Parse string to integer. + * @private + */ +function parsePos(str: string, start: number, end: number): number { + for (let i = start; i < end; i++) { + const code = str.charCodeAt(i); + if (code < 48 || code > 57) return NaN; // not a digit + } + + return Number(str.slice(start, end)); +} + +/** + * Combine overlapping & adjacent ranges. + */ +export function combineRanges(ranges: Range[]): Range[] { + 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; + + return ordered.sort(sortByRangeIndex).map(mapWithoutIndex); +} + +interface RangeWithIndex extends Range { + index: number; +} + +/** + * Map function to add index value to ranges. + * @private + */ +function mapWithIndex(range: 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: Range): 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: Range, b: 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 f882a93..0000000 --- a/test/range-parser.js +++ /dev/null @@ -1,164 +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 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 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 -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 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 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 }) - }) - }) -}) 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/**/*"] +}