Skip to content

Commit b97eaf9

Browse files
authored
Fix HttpPath.resolve(String) bugs (#9)
* Fix bugs with HttpPath.resolve(String) * This should fix broadinstitute/gatk#8751
1 parent 4abd8d1 commit b97eaf9

File tree

3 files changed

+144
-74
lines changed

3 files changed

+144
-74
lines changed

build.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,13 @@ dependencies {
3434
// use TestNG for testing
3535
testImplementation 'org.testng:testng:7.7.0'
3636
testImplementation 'org.mockito:mockito-core:5.2.0'
37+
3738
// add SLF4J implementation for testing
3839
testImplementation "org.slf4j:slf4j-simple:" + slf4jVersion
3940
testImplementation "org.wiremock:wiremock:3.3.1"
41+
42+
// include htsjdk for testing some specific issues
43+
testImplementation "com.github.samtools:htsjdk:4.1.1"
4044
}
4145

4246
// for managing the wrapper task

src/main/java/org/broadinstitute/http/nio/HttpPath.java

Lines changed: 61 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
import java.nio.file.InvalidPathException;
1313
import java.nio.file.LinkOption;
1414
import java.nio.file.Path;
15-
import java.nio.file.Paths;
1615
import java.nio.file.WatchEvent;
1716
import java.nio.file.WatchKey;
1817
import java.nio.file.WatchService;
@@ -80,11 +79,11 @@ final class HttpPath implements Path {
8079
* @param query query. May be {@code null}.
8180
* @param reference reference. May be {@code null}.
8281
* @param normalizedPath normalized path (as a byte array). Shouldn't be {@code null}.
83-
*
8482
* @implNote does not perform any check for efficiency.
8583
*/
8684
private HttpPath(final HttpFileSystem fs,
87-
final String query, final String reference,
85+
final String query,
86+
final String reference,
8887
final boolean absolute,
8988
final byte... normalizedPath) {
9089
this.fs = fs;
@@ -97,7 +96,7 @@ private HttpPath(final HttpFileSystem fs,
9796
this.absolute = absolute;
9897

9998
// normalized path bytes (shouldn't be null)
100-
this.normalizedPath = normalizedPath;
99+
this.normalizedPath = Utils.nonNull(normalizedPath, () -> "path may not be null");
101100
}
102101

103102
/**
@@ -109,7 +108,7 @@ private HttpPath(final HttpFileSystem fs,
109108
* @param reference reference component for the URL (optional).
110109
*/
111110
HttpPath(final HttpFileSystem fs, final String path, final String query,
112-
final String reference) {
111+
final String reference) {
113112
// always absolute and checking it when converting to byte[]
114113
this(Utils.nonNull(fs, () -> "null fs"), query, reference, true,
115114
getNormalizedPathBytes(Utils.nonNull(path, () -> "null path"), true));
@@ -128,7 +127,7 @@ public boolean isAbsolute() {
128127
@Override
129128
public Path getRoot() {
130129
// root is a Path with only the byte array (always absolute)
131-
return new HttpPath(fs, null, null, true);
130+
return new HttpPath(fs, null, null, true, new byte[0]);
132131
}
133132

134133
/**
@@ -352,80 +351,76 @@ public Path normalize() {
352351
/**
353352
* {@inheritDoc}
354353
*
355-
* @implNote since HttpPaths are always absolute, it's really only useful to resolve the system file path
356-
* type against them. This will always return an HttpPath so if an absolute path of another type is given as other
357-
* than this will throw {@link IllegalArgumentException}
354+
* @implNote This implementation differs from the expected behavior of the method when other is absolute. Instead
355+
* of returning other in that case it will throw {@link UnsupportedOperationException}.
358356
*/
359357
@Override
360358
public HttpPath resolve(final Path other) {
361359
if(other == null){
362360
return this;
363-
} else if(other.isAbsolute()){
364-
if(other instanceof HttpPath absolutePath){
365-
return absolutePath;
366-
} else {
367-
throw new IllegalArgumentException("Cannot a resolve an absolute path which isn't an HttpPath against an" +
368-
" HttpPath."
369-
+ "\nThis path is: " + this.toUriString()
361+
} else if(other.isAbsolute()){
362+
//Note: This violates the general contract of the method but shouldn't be important practically.
363+
throw new UnsupportedOperationException("Cannot resolve an absolute path against an http(s) path."
364+
+ "\nThis path is: " + this
370365
+ "\nThe problematic path is an instance of " + other.getClass().getName()
371366
+ "\nOther path: " + other);
372-
}
373367
} else {
374-
final URI otherUri;
375368
try {
376-
otherUri = new URI(other.toString());
369+
final URI otherUri = new URI(other.toString()); // to string is used here instead of the expected toUri because
370+
// toUri will produce normalized absolute paths in many filesystems which is
371+
// what we don't want
372+
return resolve(otherUri);
377373
} catch (URISyntaxException e) {
378374
throw new IllegalArgumentException("Can only resolve http(s) paths against fully encoded paths which are valid URIs.", e);
379375
}
380-
if(otherUri.getScheme() != null){
381-
throw new CantDealWithThisException("Attempting to resolve this against a path which is relatve but looks "
382-
+ "like it has a scheme."
383-
+ "\nThis: " + this
384-
+ "\nOther: " + other
385-
+ "\nOther interpretted as URI: " + otherUri
386-
+ "\nThis is a limitatation of the current implementation of resolve."
387-
+ "\nPlease use choose a less horrible file name or get in touch with the developers to complain.");
388-
389-
}
390-
return new HttpPath(fs, otherUri.getRawQuery(),
391-
otherUri.getRawFragment(),
392-
this.isAbsolute(),
393-
concatPaths(this.normalizedPath, getNormalizedPathBytes(otherUri.getRawPath(), false)));
394376
}
395377
}
396378

397-
/**
398-
* Thrown when an input could potentially be handled by this method but it's more
399-
* complicated to deal with than the developers had time for
400-
*/
401-
public static class CantDealWithThisException extends IllegalArgumentException{
402-
/**
403-
* @param message explain why the developers can't deal wih this
404-
*/
405-
public CantDealWithThisException(String message){
406-
super(message);
407-
}
379+
private HttpPath resolve(URI other){
380+
return new HttpPath(fs, other.getRawQuery(),
381+
other.getRawFragment(),
382+
this.isAbsolute(),
383+
concatPaths(this.normalizedPath, getNormalizedPathBytes(other.getRawPath(), false)));
408384
}
409385

410-
411386
@Override
412387
public HttpPath resolve(final String other) {
413-
return resolve(other == null ? null: Paths.get(other));
388+
return resolve(fromRelativeString(other)); // Paths.get() and the filesystem equivalent can't be used here
389+
// because we don't allow them to create relative HttpPaths
414390
}
415391

416392
@Override
417393
public Path resolveSibling(final String other){
418-
return resolveSibling(other == null ? null : Paths.get(other));
394+
return resolveSibling(fromRelativeString(other));
419395
}
396+
420397
@Override
421398
public Path relativize(final Path other) {
422399
throw new UnsupportedOperationException("Not implemented");
423400
}
424401

402+
private HttpPath fromRelativeString(final String other) {
403+
if (other == null) {
404+
return null;
405+
} else {
406+
try {
407+
final URI uri = new URI(other);
408+
if (uri.isAbsolute()) {
409+
throw new UnsupportedOperationException("Resolving absolute URI strings against an HTTP path is not supported." +
410+
"\nURI: " + uri);
411+
}
412+
return new HttpPath(getFileSystem(), uri.getRawFragment(), uri.getRawQuery(), false,
413+
getNormalizedPathBytes(uri.getRawPath(), false));
414+
} catch (URISyntaxException e) {
415+
throw new IllegalArgumentException("Cannot resolve against an invalid URI.", e);
416+
}
417+
}
418+
}
419+
425420
@Override
426421
public URI toUri() {
427422
try {
428-
return new URI(toUriString());
423+
return new URI(toUriString(true));
429424
} catch (final URISyntaxException e) {
430425
throw new IOError(e);
431426
}
@@ -436,8 +431,8 @@ public Path toAbsolutePath() {
436431
if (isAbsolute()) {
437432
return this;
438433
}
439-
// just create a new path with a different absolute status
440-
return new HttpPath(fs, query, reference, true, normalizedPath);
434+
// just fromUri a new path with a different absolute status
435+
return new HttpPath(fs, query, reference, true,normalizedPath);
441436
}
442437

443438
@Override
@@ -555,18 +550,25 @@ public int hashCode() {
555550

556551
@Override
557552
public String toString() {
558-
return toUriString();
553+
return toUriString(isAbsolute());
559554
}
560555

561-
private String toUriString() {
562-
556+
private String toUriString(boolean includeRoot) {
563557
// TODO - maybe we should cache (https://github.com/magicDGS/jsr203-http/issues/18)
564558
// adding scheme, authority and normalized path
565-
final StringBuilder sb = new StringBuilder()
566-
.append(fs.provider().getScheme()) // scheme
567-
.append("://")
568-
.append(fs.getAuthority()) // authority
569-
.append(new String(normalizedPath, HttpUtils.HTTP_PATH_CHARSET));
559+
final StringBuilder sb = new StringBuilder();
560+
if(includeRoot) {
561+
sb.append(fs.provider().getScheme()) // scheme
562+
.append("://")
563+
.append(fs.getAuthority()) // authority
564+
.append(new String(normalizedPath, HttpUtils.HTTP_PATH_CHARSET));
565+
} else if( normalizedPath.length != 0){
566+
if(normalizedPath[0] == HttpUtils.HTTP_PATH_SEPARATOR_CHAR) {
567+
sb.append(new String(normalizedPath, 1, normalizedPath.length - 1, HttpUtils.HTTP_PATH_CHARSET));
568+
} else {
569+
sb.append(new String(normalizedPath, HttpUtils.HTTP_PATH_CHARSET));
570+
}
571+
}
570572
if (query != null) {
571573
sb.append('?').append(query);
572574
}
@@ -719,4 +721,5 @@ private static byte[] concatPaths(byte[] array1, byte[] array2) {
719721
System.arraycopy(array2, 0, result, array1ModifiedLength + 1, array2.length);
720722
return result;
721723
}
724+
722725
}

0 commit comments

Comments
 (0)