@@ -159,26 +159,46 @@ private static TailFetch fetchTail(URI uri, HttpClient client) throws IOExceptio
159159 // Content-Range: bytes <start>-<end>/<total>
160160 String cr = resp .headers ().firstValue ("Content-Range" )
161161 .orElseThrow (() -> new VortexException ("206 response missing Content-Range from " + uri ));
162- String spec = cr .substring ("bytes " .length ()); // "<start>-<end>/<total>"
162+ return parseContentRange (cr , body , uri );
163+ }
164+
165+ if (status == 200 ) {
166+ // Server returned full file (no Range support)
167+ return new TailFetch (body , 0L , body .length );
168+ }
169+
170+ throw new VortexException ("HTTP " + status + " fetching tail of " + uri );
171+ }
172+
173+ /// Parses a `bytes <start>-<end>/<total>` Content-Range header from an untrusted server.
174+ /// Any structural defect (missing `bytes ` prefix, missing `-`/`/`, non-numeric fields)
175+ /// surfaces as a [VortexException] rather than a raw [NumberFormatException] or
176+ /// [StringIndexOutOfBoundsException].
177+ private static TailFetch parseContentRange (String contentRange , byte [] body , URI uri ) {
178+ try {
179+ String prefix = "bytes " ;
180+ if (!contentRange .startsWith (prefix )) {
181+ throw new VortexException ("malformed Content-Range '" + contentRange + "' from " + uri );
182+ }
183+ String spec = contentRange .substring (prefix .length ()); // "<start>-<end>/<total>"
184+ int dash = spec .indexOf ('-' );
163185 int slash = spec .indexOf ('/' );
186+ if (dash < 0 || slash < 0 || dash > slash ) {
187+ throw new VortexException ("malformed Content-Range '" + contentRange + "' from " + uri );
188+ }
189+ long start = Long .parseLong (spec .substring (0 , dash ));
190+ long end = Long .parseLong (spec .substring (dash + 1 , slash ));
164191 long total = Long .parseLong (spec .substring (slash + 1 ));
165- long start = Long .parseLong (spec .substring (0 , spec .indexOf ('-' )));
166- long end = Long .parseLong (spec .substring (spec .indexOf ('-' ) + 1 , slash ));
167192 long expected = end - start + 1 ;
168193 if (body .length != expected ) {
169194 throw new VortexException (
170195 "HTTP tail from %s: Content-Range declares %d bytes but body has %d"
171196 .formatted (uri , expected , body .length ));
172197 }
173198 return new TailFetch (body , start , total );
199+ } catch (NumberFormatException e ) {
200+ throw new VortexException ("malformed Content-Range '" + contentRange + "' from " + uri , e );
174201 }
175-
176- if (status == 200 ) {
177- // Server returned full file (no Range support)
178- return new TailFetch (body , 0L , body .length );
179- }
180-
181- throw new VortexException ("HTTP " + status + " fetching tail of " + uri );
182202 }
183203
184204 private static byte [] fetchRange (URI uri , long from , long to , HttpClient client ) throws IOException {
@@ -215,8 +235,8 @@ private static MemorySegment fetchBlob(
215235 URI uri , HttpClient client
216236 ) throws IOException {
217237 if (offset >= tailStart ) {
218- int relOffset = ( int ) ( offset - tailStart ) ;
219- return MemorySegment .ofArray (tail ). asSlice ( relOffset , length );
238+ long relOffset = offset - tailStart ;
239+ return IoBounds . slice ( MemorySegment .ofArray (tail ), relOffset , length );
220240 }
221241 byte [] bytes = fetchRange (uri , offset , offset + length - 1 , client );
222242 return MemorySegment .ofArray (bytes );
0 commit comments