UNPKG

@remix-run/headers

Version:

A toolkit for working with HTTP headers in JavaScript

155 lines (154 loc) 5.95 kB
import {} from "./header-value.js"; /** * The value of a `Range` HTTP header. * * [MDN `Range` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range) * * [HTTP/1.1 Specification](https://httpwg.org/specs/rfc9110.html#field.range) */ export class Range { unit = ''; ranges = []; /** * @param init A string or object to initialize the header */ constructor(init) { if (init) { if (typeof init === 'string') { // Parse: "bytes=200-1000" or "bytes=200-" or "bytes=-500" or "bytes=0-99,200-299" let match = init.match(/^(\w+)=(.+)$/); if (match) { this.unit = match[1]; let rangeParts = match[2].split(','); // Track if any range part is invalid to mark the entire header as malformed let hasInvalidPart = false; for (let part of rangeParts) { let rangeMatch = part.trim().match(/^(\d*)-(\d*)$/); if (!rangeMatch) { // Invalid syntax for this range part hasInvalidPart = true; continue; } let [, startStr, endStr] = rangeMatch; // At least one bound must be specified if (!startStr && !endStr) { hasInvalidPart = true; continue; } let start = startStr ? parseInt(startStr, 10) : undefined; let end = endStr ? parseInt(endStr, 10) : undefined; // If both bounds are specified, start must be <= end if (start !== undefined && end !== undefined && start > end) { hasInvalidPart = true; continue; } this.ranges.push({ start, end }); } // If any part was invalid, mark as malformed by clearing ranges if (hasInvalidPart) { this.ranges = []; } } } else { if (init.unit !== undefined) this.unit = init.unit; if (init.ranges !== undefined) this.ranges = init.ranges; } } } /** * Checks if this range can be satisfied for a resource of the given size. * * @param resourceSize The size of the resource in bytes * @return `false` if the range is malformed or all ranges are beyond the resource size */ canSatisfy(resourceSize) { // No unit or no ranges means header was malformed or empty if (!this.unit || this.ranges.length === 0) return false; // Validate all ranges first for (let range of this.ranges) { // At least one bound must be specified if (range.start === undefined && range.end === undefined) { return false; } // If both are specified, start must be <= end if (range.start !== undefined && range.end !== undefined && range.start > range.end) { return false; } } // Check if at least one range is within the resource for (let range of this.ranges) { if (range.start === undefined) { // Suffix range (e.g., "-500") is always satisfiable return true; } if (range.start < resourceSize) { // At least one range starts within the resource return true; } } return false; } /** * Normalizes the ranges for a resource of the given size. * Returns an array of ranges with resolved start and end values. * Returns an empty array if the range cannot be satisfied. * * @param resourceSize The size of the resource in bytes * @return An array of ranges with resolved start and end values */ normalize(resourceSize) { if (!this.canSatisfy(resourceSize)) { return []; } return this.ranges.map((range) => { if (range.start !== undefined && range.end !== undefined) { // Both bounds specified (e.g., "0-99") return { start: range.start, end: Math.min(range.end, resourceSize - 1), }; } else if (range.start !== undefined) { // Only start specified (e.g., "100-") return { start: range.start, end: resourceSize - 1, }; } else { // Only end specified (e.g., "-500" means last 500 bytes) let suffix = range.end; return { start: Math.max(0, resourceSize - suffix), end: resourceSize - 1, }; } }); } /** * Returns the string representation of the header value. * * @return The header value as a string */ toString() { if (!this.unit || this.ranges.length === 0) return ''; let rangeParts = this.ranges.map((range) => { if (range.start !== undefined && range.end !== undefined) { return `${range.start}-${range.end}`; } else if (range.start !== undefined) { return `${range.start}-`; } else if (range.end !== undefined) { return `-${range.end}`; } return ''; }); return `${this.unit}=${rangeParts.join(',')}`; } }