Skip to content

Commit 0765d70

Browse files
committed
fix(android): range request truncation
Closes #8371
1 parent 3d1f8d1 commit 0765d70

2 files changed

Lines changed: 240 additions & 6 deletions

File tree

android/capacitor/src/main/java/com/getcapacitor/WebViewLocalServer.java

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -348,16 +348,23 @@ private WebResourceResponse handleLocalRequest(WebResourceRequest request, PathH
348348
Map<String, String> tempResponseHeaders = handler.buildDefaultResponseHeaders();
349349
int statusCode = 206;
350350
try {
351-
int totalRange = responseStream.available();
351+
int totalSize = responseStream.available();
352352
String[] parts = rangeString.split("=");
353353
String[] streamParts = parts[1].split("-");
354-
String fromRange = streamParts[0];
355-
int range = totalRange - 1;
356-
if (streamParts.length > 1) {
357-
range = Integer.parseInt(streamParts[1]);
354+
int fromRange = Integer.parseInt(streamParts[0]);
355+
int endRange = totalSize - 1;
356+
if (streamParts.length > 1 && !streamParts[1].isEmpty()) {
357+
endRange = Integer.parseInt(streamParts[1]);
358358
}
359+
360+
// Truncate the stream at the end of the requested range.
361+
// The WebView seeks the stream to fromRange internally,
362+
// so the limit must include those bytes.
363+
int contentLength = endRange - fromRange + 1;
364+
responseStream = new BoundedInputStream(responseStream, endRange + 1);
365+
359366
tempResponseHeaders.put("Accept-Ranges", "bytes");
360-
tempResponseHeaders.put("Content-Range", "bytes " + fromRange + "-" + range + "/" + totalRange);
367+
tempResponseHeaders.put("Content-Range", "bytes " + fromRange + "-" + endRange + "/" + totalSize);
361368
} catch (IOException e) {
362369
statusCode = 404;
363370
}
@@ -750,6 +757,48 @@ public long skip(long n) throws IOException {
750757
}
751758
}
752759

760+
/**
761+
* An InputStream wrapper that limits the number of bytes that can be read.
762+
*/
763+
static class BoundedInputStream extends InputStream {
764+
765+
private final InputStream in;
766+
private long remaining;
767+
768+
public BoundedInputStream(InputStream in, long limit) {
769+
this.in = in;
770+
this.remaining = limit;
771+
}
772+
773+
@Override
774+
public int available() throws IOException {
775+
int available = in.available();
776+
return (int) Math.min(available, remaining);
777+
}
778+
779+
@Override
780+
public int read() throws IOException {
781+
if (remaining <= 0) return -1;
782+
int result = in.read();
783+
if (result != -1) remaining--;
784+
return result;
785+
}
786+
787+
@Override
788+
public int read(byte[] b, int off, int len) throws IOException {
789+
if (remaining <= 0) return -1;
790+
int toRead = (int) Math.min(len, remaining);
791+
int result = in.read(b, off, toRead);
792+
if (result > 0) remaining -= result;
793+
return result;
794+
}
795+
796+
@Override
797+
public void close() throws IOException {
798+
in.close();
799+
}
800+
}
801+
753802
// For L and above.
754803
private static class LollipopLazyInputStream extends LazyInputStream {
755804

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
package com.getcapacitor;
2+
3+
import static org.junit.Assert.*;
4+
5+
import java.io.ByteArrayInputStream;
6+
import java.io.IOException;
7+
import java.io.InputStream;
8+
import org.junit.Test;
9+
10+
public class BoundedInputStreamTest {
11+
12+
private byte[] testData() {
13+
byte[] data = new byte[256];
14+
for (int i = 0; i < data.length; i++) {
15+
data[i] = (byte) i;
16+
}
17+
return data;
18+
}
19+
20+
@Test
21+
public void readSingleByte_stopsAtLimit() throws IOException {
22+
byte[] data = testData();
23+
InputStream bounded = new WebViewLocalServer.BoundedInputStream(new ByteArrayInputStream(data), 5);
24+
25+
for (int i = 0; i < 5; i++) {
26+
assertEquals(i, bounded.read());
27+
}
28+
assertEquals(-1, bounded.read());
29+
}
30+
31+
@Test
32+
public void readByteArray_stopsAtLimit() throws IOException {
33+
byte[] data = testData();
34+
InputStream bounded = new WebViewLocalServer.BoundedInputStream(new ByteArrayInputStream(data), 10);
35+
36+
byte[] buf = new byte[20];
37+
int read = bounded.read(buf, 0, 20);
38+
assertEquals(10, read);
39+
assertEquals(-1, bounded.read(buf, 0, 20));
40+
41+
for (int i = 0; i < 10; i++) {
42+
assertEquals((byte) i, buf[i]);
43+
}
44+
}
45+
46+
@Test
47+
public void available_reflectsLimit() throws IOException {
48+
byte[] data = testData();
49+
InputStream bounded = new WebViewLocalServer.BoundedInputStream(new ByteArrayInputStream(data), 10);
50+
51+
assertEquals(10, bounded.available());
52+
53+
bounded.read();
54+
assertEquals(9, bounded.available());
55+
}
56+
57+
@Test
58+
public void limitLargerThanStream_returnsAllData() throws IOException {
59+
byte[] data = new byte[] { 1, 2, 3 };
60+
InputStream bounded = new WebViewLocalServer.BoundedInputStream(new ByteArrayInputStream(data), 100);
61+
62+
assertEquals(1, bounded.read());
63+
assertEquals(2, bounded.read());
64+
assertEquals(3, bounded.read());
65+
assertEquals(-1, bounded.read());
66+
}
67+
68+
@Test
69+
public void zeroLimit_returnsNoData() throws IOException {
70+
byte[] data = testData();
71+
InputStream bounded = new WebViewLocalServer.BoundedInputStream(new ByteArrayInputStream(data), 0);
72+
73+
assertEquals(-1, bounded.read());
74+
assertEquals(0, bounded.available());
75+
}
76+
77+
@Test
78+
public void close_closesUnderlying() throws IOException {
79+
final boolean[] closed = { false };
80+
InputStream inner = new ByteArrayInputStream(new byte[10]) {
81+
@Override
82+
public void close() throws IOException {
83+
closed[0] = true;
84+
super.close();
85+
}
86+
};
87+
InputStream bounded = new WebViewLocalServer.BoundedInputStream(inner, 5);
88+
bounded.close();
89+
assertTrue(closed[0]);
90+
}
91+
92+
/**
93+
* Simulates the WebView range request behavior:
94+
* WebView seeks (consumes) fromRange bytes, then reads contentLength bytes.
95+
* The BoundedInputStream limit should be endRange + 1 to account for both.
96+
*/
97+
@Test
98+
public void simulateWebViewRangeSeek() throws IOException {
99+
// 1000-byte file, request bytes=200-499
100+
int totalSize = 1000;
101+
int fromRange = 200;
102+
int endRange = 499;
103+
int contentLength = endRange - fromRange + 1; // 300
104+
105+
byte[] fileData = new byte[totalSize];
106+
for (int i = 0; i < totalSize; i++) {
107+
fileData[i] = (byte) (i & 0xFF);
108+
}
109+
110+
InputStream bounded = new WebViewLocalServer.BoundedInputStream(
111+
new ByteArrayInputStream(fileData),
112+
endRange + 1 // 500
113+
);
114+
115+
// WebView seeks by consuming fromRange bytes
116+
byte[] seekBuf = new byte[fromRange];
117+
int seeked = 0;
118+
while (seeked < fromRange) {
119+
int r = bounded.read(seekBuf, seeked, fromRange - seeked);
120+
if (r == -1) break;
121+
seeked += r;
122+
}
123+
assertEquals(fromRange, seeked);
124+
125+
// WebView then reads the actual content
126+
byte[] content = new byte[contentLength];
127+
int totalRead = 0;
128+
while (totalRead < contentLength) {
129+
int r = bounded.read(content, totalRead, contentLength - totalRead);
130+
if (r == -1) break;
131+
totalRead += r;
132+
}
133+
assertEquals(contentLength, totalRead);
134+
135+
// Verify data is correct (bytes 200-499)
136+
for (int i = 0; i < contentLength; i++) {
137+
assertEquals((byte) ((fromRange + i) & 0xFF), content[i]);
138+
}
139+
140+
// No more data should be available
141+
assertEquals(-1, bounded.read());
142+
}
143+
144+
/**
145+
* When end range is omitted (request to end of file),
146+
* the limit is totalSize and the full remainder is returned.
147+
*/
148+
@Test
149+
public void simulateWebViewRangeSeek_openEnded() throws IOException {
150+
int totalSize = 500;
151+
int fromRange = 300;
152+
int endRange = totalSize - 1; // 499
153+
154+
byte[] fileData = new byte[totalSize];
155+
for (int i = 0; i < totalSize; i++) {
156+
fileData[i] = (byte) (i & 0xFF);
157+
}
158+
159+
InputStream bounded = new WebViewLocalServer.BoundedInputStream(
160+
new ByteArrayInputStream(fileData),
161+
endRange + 1 // = totalSize
162+
);
163+
164+
// WebView seeks
165+
for (int i = 0; i < fromRange; i++) {
166+
int b = bounded.read();
167+
assertNotEquals(-1, b);
168+
}
169+
170+
// Read remainder
171+
int contentLength = endRange - fromRange + 1; // 200
172+
byte[] content = new byte[contentLength];
173+
int totalRead = 0;
174+
while (totalRead < contentLength) {
175+
int r = bounded.read(content, totalRead, contentLength - totalRead);
176+
if (r == -1) break;
177+
totalRead += r;
178+
}
179+
assertEquals(contentLength, totalRead);
180+
181+
// Verify last byte is correct
182+
assertEquals((byte) (endRange & 0xFF), content[contentLength - 1]);
183+
assertEquals(-1, bounded.read());
184+
}
185+
}

0 commit comments

Comments
 (0)