Skip to content

Commit aafc567

Browse files
authored
fix: byte range request end should never equal file size (#24)
* fix: request-headers byte range parsing * fix: response-headers byte parsing * fix: request-headers * fix: byteRangeContext tests * fix: range-requests tests
1 parent 8bf9c9f commit aafc567

7 files changed

+102
-69
lines changed

packages/verified-fetch/src/utils/byte-range-context.ts

+33-35
Original file line numberDiff line numberDiff line change
@@ -116,30 +116,14 @@ export class ByteRangeContext {
116116
return body
117117
}
118118

119+
// TODO: we should be able to use this.offset and this.length to slice the body
119120
private getSlicedBody <T extends SliceableBody>(body: T): SliceableBody {
120-
if (this.isPrefixLengthRequest) {
121-
this.log.trace('sliced body with byteStart %o', this.byteStart)
122-
return body.slice(this.offset) satisfies SliceableBody
123-
}
124-
if (this.isSuffixLengthRequest && this.length != null) {
125-
this.log.trace('sliced body with length %o', -this.length)
126-
return body.slice(-this.length) satisfies SliceableBody
127-
}
128121
const offset = this.byteStart ?? 0
129122
const length = this.byteEnd == null ? undefined : this.byteEnd + 1
130123
this.log.trace('returning body with offset %o and length %o', offset, length)
131-
132124
return body.slice(offset, length) satisfies SliceableBody
133125
}
134126

135-
private get isSuffixLengthRequest (): boolean {
136-
return this.requestRangeStart == null && this.requestRangeEnd != null
137-
}
138-
139-
private get isPrefixLengthRequest (): boolean {
140-
return this.requestRangeStart != null && this.requestRangeEnd == null
141-
}
142-
143127
/**
144128
* Sometimes, we need to set the fileSize explicitly because we can't calculate
145129
* the size of the body (e.g. for unixfs content where we call .stat).
@@ -162,7 +146,10 @@ export class ByteRangeContext {
162146
if (this.byteStart < 0) {
163147
return false
164148
}
165-
if (this._fileSize != null && this.byteStart > this._fileSize) {
149+
if (this._fileSize != null && this.byteStart >= this._fileSize) {
150+
return false
151+
}
152+
if (this.byteEnd != null && this.byteStart > this.byteEnd) {
166153
return false
167154
}
168155
}
@@ -174,7 +161,10 @@ export class ByteRangeContext {
174161
if (this.byteEnd < 0) {
175162
return false
176163
}
177-
if (this._fileSize != null && this.byteEnd > this._fileSize) {
164+
if (this._fileSize != null && this.byteEnd >= this._fileSize) {
165+
return false
166+
}
167+
if (this.byteStart != null && this.byteEnd < this.byteStart) {
178168
return false
179169
}
180170
}
@@ -214,6 +204,10 @@ export class ByteRangeContext {
214204
return false
215205
}
216206
}
207+
if (this.byteEnd == null && this.byteStart == null && this.byteSize == null) {
208+
this.log.trace('invalid range request, could not calculate byteStart, byteEnd, or byteSize')
209+
return false
210+
}
217211

218212
return true
219213
}
@@ -224,16 +218,6 @@ export class ByteRangeContext {
224218
* 2. slicing the body
225219
*/
226220
public get offset (): number {
227-
if (this.byteStart === 0) {
228-
return 0
229-
}
230-
if (this.isPrefixLengthRequest || this.isSuffixLengthRequest) {
231-
if (this.byteStart != null) {
232-
// we have to subtract by 1 because the offset is inclusive
233-
return this.byteStart - 1
234-
}
235-
}
236-
237221
return this.byteStart ?? 0
238222
}
239223

@@ -243,7 +227,14 @@ export class ByteRangeContext {
243227
* 2. slicing the body
244228
*/
245229
public get length (): number | undefined {
246-
return this.byteSize ?? undefined
230+
if (this.byteEnd != null && this.byteStart != null && this.byteStart === this.byteEnd) {
231+
return 1
232+
}
233+
if (this.byteEnd != null) {
234+
return this.byteEnd + 1
235+
}
236+
237+
return this.byteSize != null ? this.byteSize - 1 : undefined
247238
}
248239

249240
/**
@@ -269,11 +260,18 @@ export class ByteRangeContext {
269260
return
270261
}
271262

272-
const { start, end, byteSize } = calculateByteRangeIndexes(this.requestRangeStart ?? undefined, this.requestRangeEnd ?? undefined, this._fileSize ?? undefined)
273-
this.log.trace('set byteStart to %o, byteEnd to %o, byteSize to %o', start, end, byteSize)
274-
this.byteStart = start
275-
this.byteEnd = end
276-
this.byteSize = byteSize
263+
try {
264+
const { start, end, byteSize } = calculateByteRangeIndexes(this.requestRangeStart ?? undefined, this.requestRangeEnd ?? undefined, this._fileSize ?? undefined)
265+
this.log.trace('set byteStart to %o, byteEnd to %o, byteSize to %o', start, end, byteSize)
266+
this.byteStart = start
267+
this.byteEnd = end
268+
this.byteSize = byteSize
269+
} catch (e) {
270+
this.log.error('error setting offset details: %o', e)
271+
this.byteStart = undefined
272+
this.byteEnd = undefined
273+
this.byteSize = undefined
274+
}
277275
}
278276

279277
/**

packages/verified-fetch/src/utils/request-headers.ts

+22-10
Original file line numberDiff line numberDiff line change
@@ -23,29 +23,41 @@ export function getHeader (headers: HeadersInit | undefined, header: string): st
2323
* 2. the start index of the range. // inclusive
2424
* 3. the end index of the range. // inclusive
2525
*/
26+
// eslint-disable-next-line complexity
2627
export function calculateByteRangeIndexes (start: number | undefined, end: number | undefined, fileSize?: number): { byteSize?: number, start?: number, end?: number } {
27-
if (start != null && end != null) {
28-
if (start > end) {
29-
throw new Error('Invalid range')
30-
}
28+
if ((start ?? 0) > (end ?? Infinity)) {
29+
throw new Error('Invalid range: Range-start index is greater than range-end index.')
30+
}
31+
if (start != null && (end ?? 0) >= (fileSize ?? Infinity)) {
32+
throw new Error('Invalid range: Range-end index is greater than or equal to the size of the file.')
33+
}
34+
if (start == null && (end ?? 0) > (fileSize ?? Infinity)) {
35+
throw new Error('Invalid range: Range-end index is greater than the size of the file.')
36+
}
37+
if (start != null && start < 0) {
38+
throw new Error('Invalid range: Range-start index cannot be negative.')
39+
}
3140

41+
if (start != null && end != null) {
3242
return { byteSize: end - start + 1, start, end }
3343
} else if (start == null && end != null) {
3444
// suffix byte range requested
3545
if (fileSize == null) {
3646
return { end }
3747
}
38-
const result = { byteSize: end, start: fileSize - end + 1, end: fileSize }
39-
return result
48+
if (end === fileSize) {
49+
return { byteSize: fileSize, start: 0, end: fileSize - 1 }
50+
}
51+
return { byteSize: end, start: fileSize - end, end: fileSize - 1 }
4052
} else if (start != null && end == null) {
4153
if (fileSize == null) {
54+
// we only have the start index, and no fileSize, so we can't return a valid range.
4255
return { start }
4356
}
44-
const byteSize = fileSize - start + 1
45-
const end = fileSize
57+
const end = fileSize - 1
58+
const byteSize = fileSize - start
4659
return { byteSize, start, end }
4760
}
4861

49-
// both start and end are undefined
50-
return { byteSize: fileSize }
62+
return { byteSize: fileSize, start: 0, end: fileSize != null ? fileSize - 1 : 0 }
5163
}

packages/verified-fetch/src/utils/response-headers.ts

+12-2
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,30 @@
77
export function getContentRangeHeader ({ byteStart, byteEnd, byteSize }: { byteStart: number | undefined, byteEnd: number | undefined, byteSize: number | undefined }): string {
88
const total = byteSize ?? '*' // if we don't know the total size, we should use *
99

10+
if ((byteEnd ?? 0) >= (byteSize ?? Infinity)) {
11+
throw new Error('Invalid range: Range-end index is greater than or equal to the size of the file.')
12+
}
13+
if ((byteStart ?? 0) >= (byteSize ?? Infinity)) {
14+
throw new Error('Invalid range: Range-start index is greater than or equal to the size of the file.')
15+
}
16+
1017
if (byteStart != null && byteEnd == null) {
1118
// only byteStart in range
1219
if (byteSize == null) {
1320
return `bytes */${total}`
1421
}
15-
return `bytes ${byteStart}-${byteSize}/${byteSize}`
22+
return `bytes ${byteStart}-${byteSize - 1}/${byteSize}`
1623
}
1724

1825
if (byteStart == null && byteEnd != null) {
1926
// only byteEnd in range
2027
if (byteSize == null) {
2128
return `bytes */${total}`
2229
}
23-
return `bytes ${byteSize - byteEnd + 1}-${byteSize}/${byteSize}`
30+
const end = byteSize - 1
31+
const start = end - byteEnd + 1
32+
33+
return `bytes ${start}-${end}/${byteSize}`
2434
}
2535

2636
if (byteStart == null && byteEnd == null) {

packages/verified-fetch/test/range-requests.spec.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,13 @@ describe('range requests', () => {
7070
},
7171
{
7272
byteSize: 8,
73-
contentRange: 'bytes 4-11/11',
73+
contentRange: 'bytes 4-10/11',
7474
rangeHeader: 'bytes=4-',
75-
bytes: new Uint8Array([3, 4, 5, 6, 7, 8, 9, 10])
75+
bytes: new Uint8Array([4, 5, 6, 7, 8, 9, 10])
7676
},
7777
{
7878
byteSize: 9,
79-
contentRange: 'bytes 3-11/11',
79+
contentRange: 'bytes 2-10/11',
8080
rangeHeader: 'bytes=-9',
8181
bytes: new Uint8Array([2, 3, 4, 5, 6, 7, 8, 9, 10])
8282
}

packages/verified-fetch/test/utils/byte-range-context.spec.ts

+8-9
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ describe('ByteRangeContext', () => {
4545
const array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
4646
const uint8arrayRangeTests = [
4747
// full ranges:
48-
{ type: 'Uint8Array', range: 'bytes=0-11', contentRange: 'bytes 0-11/11', body: new Uint8Array(array), expected: new Uint8Array(array) },
49-
{ type: 'Uint8Array', range: 'bytes=-11', contentRange: 'bytes 1-11/11', body: new Uint8Array(array), expected: new Uint8Array(array) },
50-
{ type: 'Uint8Array', range: 'bytes=0-', contentRange: 'bytes 0-11/11', body: new Uint8Array(array), expected: new Uint8Array(array) },
48+
{ type: 'Uint8Array', range: 'bytes=0-10', contentRange: 'bytes 0-10/11', body: new Uint8Array(array), expected: new Uint8Array(array) },
49+
{ type: 'Uint8Array', range: 'bytes=-11', contentRange: 'bytes 0-10/11', body: new Uint8Array(array), expected: new Uint8Array(array) },
50+
{ type: 'Uint8Array', range: 'bytes=0-', contentRange: 'bytes 0-10/11', body: new Uint8Array(array), expected: new Uint8Array(array) },
5151

5252
// partial ranges:
5353
{ type: 'Uint8Array', range: 'bytes=0-1', contentRange: 'bytes 0-1/11', body: new Uint8Array(array), expected: new Uint8Array([1, 2]) },
@@ -60,13 +60,13 @@ describe('ByteRangeContext', () => {
6060
{ type: 'Uint8Array', range: 'bytes=0-8', contentRange: 'bytes 0-8/11', body: new Uint8Array(array), expected: new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9]) },
6161
{ type: 'Uint8Array', range: 'bytes=0-9', contentRange: 'bytes 0-9/11', body: new Uint8Array(array), expected: new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) },
6262
{ type: 'Uint8Array', range: 'bytes=0-10', contentRange: 'bytes 0-10/11', body: new Uint8Array(array), expected: new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) },
63-
{ type: 'Uint8Array', range: 'bytes=1-', contentRange: 'bytes 1-11/11', body: new Uint8Array(array), expected: new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) },
64-
{ type: 'Uint8Array', range: 'bytes=2-', contentRange: 'bytes 2-11/11', body: new Uint8Array(array), expected: new Uint8Array([2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) },
65-
{ type: 'Uint8Array', range: 'bytes=-2', contentRange: 'bytes 10-11/11', body: new Uint8Array(array), expected: new Uint8Array(array.slice(-2)) },
63+
{ type: 'Uint8Array', range: 'bytes=1-', contentRange: 'bytes 1-10/11', body: new Uint8Array(array), expected: new Uint8Array([2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) },
64+
{ type: 'Uint8Array', range: 'bytes=2-', contentRange: 'bytes 2-10/11', body: new Uint8Array(array), expected: new Uint8Array([3, 4, 5, 6, 7, 8, 9, 10, 11]) },
65+
{ type: 'Uint8Array', range: 'bytes=-2', contentRange: 'bytes 9-10/11', body: new Uint8Array(array), expected: new Uint8Array(array.slice(-2)) },
6666

6767
// single byte ranges:
6868
{ type: 'Uint8Array', range: 'bytes=1-1', contentRange: 'bytes 1-1/11', body: new Uint8Array(array), expected: new Uint8Array(array.slice(1, 2)) },
69-
{ type: 'Uint8Array', range: 'bytes=-1', contentRange: 'bytes 11-11/11', body: new Uint8Array(array), expected: new Uint8Array(array.slice(-1)) }
69+
{ type: 'Uint8Array', range: 'bytes=-1', contentRange: 'bytes 10-10/11', body: new Uint8Array(array), expected: new Uint8Array(array.slice(-1)) }
7070

7171
]
7272
const validRanges = [
@@ -138,12 +138,11 @@ describe('ByteRangeContext', () => {
138138
})
139139
const stat = await fs.stat(cid)
140140
context.setFileSize(stat.fileSize)
141-
142141
context.setBody(await getBodyStream(context.offset, context.length))
143142
const response = new Response(context.getBody())
144143
const bodyResult = await response.arrayBuffer()
145-
expect(new Uint8Array(bodyResult)).to.deep.equal(expected)
146144
expect(context.contentRangeHeaderValue).to.equal(contentRange)
145+
expect(new Uint8Array(bodyResult)).to.deep.equal(expected)
147146
})
148147
})
149148
})

packages/verified-fetch/test/utils/request-headers.spec.ts

+15-8
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ describe('request-headers', () => {
3232
describe('calculateByteRangeIndexes', () => {
3333
const testCases = [
3434
// Range: bytes=5-
35-
{ start: 5, end: undefined, fileSize: 10, expected: { byteSize: 6, start: 5, end: 10 } },
35+
{ start: 5, end: undefined, fileSize: 10, expected: { byteSize: 5, start: 5, end: 9 } },
3636
// Range: bytes=-5
37-
{ start: undefined, end: 5, fileSize: 10, expected: { byteSize: 5, start: 6, end: 10 } },
37+
{ start: undefined, end: 5, fileSize: 10, expected: { byteSize: 5, start: 5, end: 9 } },
3838
// Range: bytes=0-0
3939
{ start: 0, end: 0, fileSize: 10, expected: { byteSize: 1, start: 0, end: 0 } },
4040
// Range: bytes=5- with unknown filesize
@@ -44,18 +44,25 @@ describe('request-headers', () => {
4444
// Range: bytes=0-0 with unknown filesize
4545
{ start: 0, end: 0, fileSize: undefined, expected: { byteSize: 1, start: 0, end: 0 } },
4646
// Range: bytes=-9 & fileSize=11
47-
{ start: undefined, end: 9, fileSize: 11, expected: { byteSize: 9, start: 3, end: 11 } },
48-
// Range: bytes=0-11 & fileSize=11
49-
{ start: 0, end: 11, fileSize: 11, expected: { byteSize: 12, start: 0, end: 11 } }
47+
{ start: undefined, end: 9, fileSize: 11, expected: { byteSize: 9, start: 2, end: 10 } },
48+
// Range: bytes=-11 & fileSize=11
49+
{ start: undefined, end: 11, fileSize: 11, expected: { byteSize: 11, start: 0, end: 10 } },
50+
// Range: bytes=-2 & fileSize=11
51+
{ start: undefined, end: 2, fileSize: 11, expected: { byteSize: 2, start: 9, end: 10 } },
52+
// Range request with no range (added for coverage)
53+
{ start: undefined, end: undefined, fileSize: 10, expected: { byteSize: 10, start: 0, end: 9 } }
54+
5055
]
5156
testCases.forEach(({ start, end, fileSize, expected }) => {
5257
it(`should return expected result for bytes=${start ?? ''}-${end ?? ''} and fileSize=${fileSize}`, () => {
53-
const result = calculateByteRangeIndexes(start, end, fileSize)
54-
expect(result).to.deep.equal(expected)
58+
expect(calculateByteRangeIndexes(start, end, fileSize)).to.deep.equal(expected)
5559
})
5660
})
5761
it('throws error for invalid range', () => {
58-
expect(() => calculateByteRangeIndexes(5, 4, 10)).to.throw('Invalid range')
62+
expect(() => calculateByteRangeIndexes(5, 4, 10)).to.throw('Invalid range: Range-start index is greater than range-end index.')
63+
expect(() => calculateByteRangeIndexes(5, 11, 11)).to.throw('Invalid range: Range-end index is greater than or equal to the size of the file.')
64+
expect(() => calculateByteRangeIndexes(undefined, 12, 11)).to.throw('Invalid range: Range-end index is greater than the size of the file.')
65+
expect(() => calculateByteRangeIndexes(-1, 5, 10)).to.throw('Invalid range: Range-start index cannot be negative.')
5966
})
6067
})
6168
})

packages/verified-fetch/test/utils/response-headers.spec.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ describe('response-headers', () => {
1111
})
1212

1313
it('should return correct content range header when only byteEnd and byteSize are provided', () => {
14-
expect(getContentRangeHeader({ byteStart: undefined, byteEnd: 9, byteSize: 11 })).to.equal('bytes 3-11/11')
14+
expect(getContentRangeHeader({ byteStart: undefined, byteEnd: 9, byteSize: 11 })).to.equal('bytes 2-10/11')
1515
})
1616

1717
it('should return correct content range header when only byteStart and byteSize are provided', () => {
18-
expect(getContentRangeHeader({ byteStart: 5, byteEnd: undefined, byteSize: 11 })).to.equal('bytes 5-11/11')
18+
expect(getContentRangeHeader({ byteStart: 5, byteEnd: undefined, byteSize: 11 })).to.equal('bytes 5-10/11')
1919
})
2020

2121
it('should return correct content range header when only byteStart is provided', () => {
@@ -29,5 +29,12 @@ describe('response-headers', () => {
2929
it('should return content range header with when only byteSize is provided', () => {
3030
expect(getContentRangeHeader({ byteStart: undefined, byteEnd: undefined, byteSize: 50 })).to.equal('bytes */50')
3131
})
32+
33+
it('should not allow range-end to equal or exceed the size of the file', () => {
34+
expect(() => getContentRangeHeader({ byteStart: 0, byteEnd: 11, byteSize: 11 })).to.throw('Invalid range') // byteEnd is equal to byteSize
35+
expect(() => getContentRangeHeader({ byteStart: undefined, byteEnd: 11, byteSize: 11 })).to.throw('Invalid range') // byteEnd is equal to byteSize
36+
expect(() => getContentRangeHeader({ byteStart: undefined, byteEnd: 12, byteSize: 11 })).to.throw('Invalid range') // byteEnd is greater than byteSize
37+
expect(() => getContentRangeHeader({ byteStart: 11, byteEnd: undefined, byteSize: 11 })).to.throw('Invalid range') // byteEnd is greater than byteSize
38+
})
3239
})
3340
})

0 commit comments

Comments
 (0)