Skip to content
Open
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
17 changes: 8 additions & 9 deletions .github/workflows/ci-module.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
name: ci

on:
push:
branches:
- master
pull_request:
workflow_dispatch:
push:
branches:
- master
- next
pull_request:
workflow_dispatch:

jobs:
test:
uses: hapijs/.github/.github/workflows/ci-module.yml@master
with:
min-node-version: 14
test:
uses: hapijs/.github/.github/workflows/ci-module.yml@min-node-22-hapi-21
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
**/node_modules
**/package-lock.json

coverage.*
coverage/

**/.DS_Store
**/._*
Expand Down
1 change: 1 addition & 0 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,5 @@ Returns an array of objects with the properties `from` and `to`, which specify t
### `new Ammo.Stream(range)`

Creates a [`Transform Stream`](https://nodejs.org/api/stream.html) that extracts the portion of a piped in stream within `range`, where:

- `range` - an object with the properties `from` and `to` that specify the range of the piped in stream to read. Objects returned by `Ammo.header` can be passed into `range`.
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,3 @@
- [Changelog](https://hapi.dev/family/ammo/changelog/)
- [Project policies](https://hapi.dev/policies/)
- [Free and commercial support options](https://hapi.dev/support/)

8 changes: 8 additions & 0 deletions oxfmt.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import DefaultOxfmtConfig from '@hapi/oxc-plugin/oxfmt';
import { defineConfig } from 'oxfmt';

import type { OxfmtConfig } from 'oxfmt';

export default defineConfig({
...DefaultOxfmtConfig,
}) as OxfmtConfig;
11 changes: 11 additions & 0 deletions oxlint.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import HapiRecommended from '@hapi/oxc-plugin/oxlint';
import { defineConfig } from 'oxlint';

import type { OxlintConfig } from 'oxlint';

export default defineConfig({
extends: [HapiRecommended],
env: {
...HapiRecommended.env,
},
}) as OxlintConfig;
84 changes: 49 additions & 35 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,37 +1,51 @@
{
"name": "@hapi/ammo",
"description": "HTTP Range processing utilities",
"version": "6.0.1",
"repository": "git://github.com/hapijs/ammo",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"keywords": [
"http",
"range",
"utilities"
],
"files": [
"lib"
],
"eslintConfig": {
"extends": [
"plugin:@hapi/module"
]
},
"dependencies": {
"@hapi/hoek": "^11.0.2"
},
"devDependencies": {
"@hapi/code": "^9.0.3",
"@hapi/eslint-plugin": "^6.0.0",
"@hapi/lab": "^25.1.2",
"@hapi/wreck": "^18.0.1",
"@types/node": "^17.0.31",
"typescript": "~4.6.4"
},
"scripts": {
"test": "lab -a @hapi/code -t 100 -L -Y",
"test-cov-html": "lab -a @hapi/code -r html -o coverage.html"
},
"license": "BSD-3-Clause"
"name": "@hapi/ammo",
"version": "6.0.1",
"description": "HTTP Range processing utilities",
"keywords": [
"http",
"range",
"utilities"
],
"license": "BSD-3-Clause",
"repository": "git://github.com/hapijs/ammo",
"files": [
"src",
"API.md",
"README.md"
],
"type": "module",
"types": "src/index.d.ts",
"exports": {
".": {
"types": "./src/index.d.ts",
"default": "./src/index.js"
},
"./package.json": "./package.json"
},
"scripts": {
"test": "vitest run --coverage",
"typecheck": "tsc --noEmit",
"lint": "oxlint",
"lint:fix": "oxlint --fix",
"fmt": "oxfmt --check",
"fmt:fix": "oxfmt",
"check": "npm run lint && npm run fmt && npm run typecheck && npm test"
},
"dependencies": {
"@hapi/hoek": "^12.0.0-rc.0"
},
"devDependencies": {
"@hapi/oxc-plugin": "^1.0.4",
"@hapi/wreck": "^18.1.0",
"@types/node": "^22",
"@vitest/coverage-v8": "^4.1.10",
"oxfmt": "^0.61.0",
"oxlint": "^1.76.0",
"typescript": "^6.0.3",
"vitest": "^4.1.10"
},
"engines": {
"node": ">=22"
}
}
29 changes: 9 additions & 20 deletions lib/index.d.ts → src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,29 @@

import * as Stream from 'stream';


/**
* Parses an HTTP Range header.
*
* @param header - the HTTP Range header.
* @param length - the payload length.
*
* @returns an array of range objects.
*
* @param header - The HTTP Range header.
* @param length - The payload length.
* @returns An array of range objects.
*/
export function header(header: string, length: number): null | Range[];


/**
* A transform stream taking full payload and returning the requested range.
*/
/** A transform stream taking full payload and returning the requested range. */
export class Clip extends Stream.Transform {

/**
* Constructs a new transform steam.
*
* @param range - the requested range.
*
* @param range - The requested range.
*/
constructor(range: Range);
}


export interface Range {
/**
* The range start position (inclusive).
*/
/** The range start position (inclusive). */
readonly from: number;

/**
* The range end position (inclusive).
*/
/** The range end position (inclusive). */
readonly to: number;
}
72 changes: 26 additions & 46 deletions lib/index.js → src/index.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,20 @@
'use strict';

const Stream = require('stream');

const Hoek = require('@hapi/hoek');


const internals = {};
import Stream from 'node:stream';

import * as Hoek from '@hapi/hoek';

// RFC 7233 (https://tools.ietf.org/html/rfc7233#appendix-D)
//
// Range = "bytes" "=" byte-range-set
// byte-range-set = *( "," OWS ) byte-range-spec *( OWS "," [ OWS byte-range-spec ] )
// byte-range-spec = ( 1*DIGIT "-" [ 1*DIGIT ] ) / ( "-" 1*DIGIT )


// 12 3 3 4 425 6 7 7 8 865 1
internals.headerRx = /^bytes=[\s,]*((?:(?:\d+\-\d*)|(?:\-\d+))(?:\s*,\s*(?:(?:\d+\-\d*)|(?:\-\d+)))*)$/i;


exports.header = function (header, length) {
const headerRx = /^bytes=[\s,]*((?:(?:\d+\-\d*)|(?:\-\d+))(?:\s*,\s*(?:(?:\d+\-\d*)|(?:\-\d+)))*)$/i;

export function header(header, length) {
// Parse header

const parts = internals.headerRx.exec(header);
const parts = headerRx.exec(header);
if (!parts) {
return null;
}
Expand All @@ -45,19 +36,18 @@ exports.header = function (header, length) {

if (range[1]) {
to = parseInt(range[1], 10);
if (from !== undefined) { // Can be 0
if (from !== undefined) {
// Can be 0
// From-To
if (to > lastPos) {
to = lastPos;
}
}
else {
} else {
// -To
from = length - to;
to = lastPos;
}
}
else {
} else {
// From-
to = lastPos;
}
Expand All @@ -66,7 +56,7 @@ exports.header = function (header, length) {
return null;
}

result.push(new internals.Range(from, to));
result.push(new Range(from, to));
}

if (result.length === 1) {
Expand All @@ -83,33 +73,26 @@ exports.header = function (header, length) {
const before = result[i - 1];
if (current.from <= before.to + 1) {
before.to = current.to;
}
else {
} else {
consolidated.unshift(current);
}
}

consolidated.unshift(result[0]);

return consolidated;
};


internals.Range = class {
}

class Range {
constructor(from, to) {

this.from = from;
this.to = to;
}
};


exports.Clip = class extends Stream.Transform {
}

export class Clip extends Stream.Transform {
constructor(range) {

if (!(range instanceof internals.Range)) {
if (!(range instanceof Range)) {
Hoek.assert(typeof range === 'object', 'Expected "range" object');

const from = range.from ?? 0;
Expand All @@ -122,7 +105,7 @@ exports.Clip = class extends Stream.Transform {

Hoek.assert(to >= from, '"range.to" must be greater than or equal to "range.from"');

range = new internals.Range(from, to);
range = new Range(from, to);
}

super();
Expand All @@ -136,37 +119,34 @@ exports.Clip = class extends Stream.Transform {
}

_transform(chunk, encoding, done) {

try {
internals.processChunk(this, chunk);
}
catch (err) {
processChunk(this, chunk);
} catch (err) {
return done(err);
}

return done();
}

_flush(done) {

this._pipes.clear();
done();
}
};


internals.processChunk = function (stream, chunk) {
}

function processChunk(stream, chunk) {
// Read desired range from a stream

const pos = stream._next;
stream._next = stream._next + chunk.length;

if (stream._next <= stream._range.from) { // Before range
if (stream._next <= stream._range.from) {
// Before range
return;
}

if (pos > stream._range.to) { // After range
if (pos > stream._range.to) {
// After range
for (const pipe of stream._pipes) {
pipe.unpipe(stream);
}
Expand All @@ -182,4 +162,4 @@ internals.processChunk = function (stream, chunk) {
const to = Math.min(chunk.length, stream._range.to - pos + 1);

stream.push(chunk.slice(from, to));
};
}
Loading
Loading