Skip to content

Commit 35a60e6

Browse files
committed
Add start option validation proof
1 parent ed7af78 commit 35a60e6

8 files changed

Lines changed: 541 additions & 1 deletion

File tree

example/build.gradle

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ tasks.register('api2DevdockTusDetailedError', JavaExec) {
5454
workingDir = rootProject.projectDir
5555
}
5656

57+
tasks.register('api2DevdockTusStartOptionValidation', JavaExec) {
58+
classpath = sourceSets.main.runtimeClasspath
59+
mainClass = 'io.tus.java.example.Api2DevdockTusStartOptionValidation'
60+
workingDir = rootProject.projectDir
61+
}
62+
5763
tasks.register('api2DevdockTusRequestLifecycleHooks', JavaExec) {
5864
classpath = sourceSets.main.runtimeClasspath
5965
mainClass = 'io.tus.java.example.Api2DevdockTusRequestLifecycleHooks'

example/src/main/java/io/tus/java/example/Api2DevdockScenario.java

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,53 @@ static String conformanceInputStringOption(JSONObject conformanceScenario, Strin
124124
return scalarString(conformanceInputOption(conformanceScenario, key));
125125
}
126126

127+
static boolean conformanceInputBooleanOption(
128+
JSONObject conformanceScenario,
129+
String key,
130+
boolean defaultValue
131+
) {
132+
final Object value = conformanceInputOptionOrNull(conformanceScenario, key);
133+
if (value == null || JSONObject.NULL.equals(value)) {
134+
return defaultValue;
135+
}
136+
137+
if (!(value instanceof Boolean)) {
138+
throw new IllegalArgumentException(
139+
"conformance input option " + key + " is not a boolean"
140+
);
141+
}
142+
143+
return ((Boolean) value).booleanValue();
144+
}
145+
146+
static int conformanceInputIntegerOption(
147+
JSONObject conformanceScenario,
148+
String key,
149+
int defaultValue
150+
) {
151+
final Object value = conformanceInputOptionOrNull(conformanceScenario, key);
152+
if (value == null || JSONObject.NULL.equals(value)) {
153+
return defaultValue;
154+
}
155+
156+
if (!(value instanceof Number)) {
157+
throw new IllegalArgumentException(
158+
"conformance input option " + key + " is not a number"
159+
);
160+
}
161+
162+
return ((Number) value).intValue();
163+
}
164+
165+
static String conformanceInputStringOptionOrNull(JSONObject conformanceScenario, String key) {
166+
final Object value = conformanceInputOptionOrNull(conformanceScenario, key);
167+
if (value == null || JSONObject.NULL.equals(value)) {
168+
return null;
169+
}
170+
171+
return scalarString(value);
172+
}
173+
127174
static byte[] scenarioBytes(JSONObject uploadConfig) {
128175
final JSONObject source = uploadConfig.getJSONObject("source");
129176
final String kind = source.getString("kind");
@@ -140,6 +187,15 @@ static byte[] scenarioBytes(JSONObject uploadConfig) {
140187
}
141188

142189
private static Object conformanceInputOption(JSONObject conformanceScenario, String key) {
190+
final Object value = conformanceInputOptionOrNull(conformanceScenario, key);
191+
if (value != null) {
192+
return value;
193+
}
194+
195+
throw new IllegalArgumentException("missing conformance input option " + key);
196+
}
197+
198+
private static Object conformanceInputOptionOrNull(JSONObject conformanceScenario, String key) {
143199
final JSONArray entries = conformanceScenario.getJSONArray("inputOptionEntries");
144200
for (int index = 0; index < entries.length(); index++) {
145201
final JSONObject entry = entries.getJSONObject(index);
@@ -148,7 +204,7 @@ private static Object conformanceInputOption(JSONObject conformanceScenario, Str
148204
}
149205
}
150206

151-
throw new IllegalArgumentException("missing conformance input option " + key);
207+
return null;
152208
}
153209

154210
private static JSONObject conformanceInputJSONObjectOption(
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package io.tus.java.example;
2+
3+
import io.tus.java.client.TusClient;
4+
import io.tus.java.client.TusStartOptions;
5+
import io.tus.java.client.TusUpload;
6+
7+
import org.json.JSONObject;
8+
9+
import java.io.ByteArrayInputStream;
10+
import java.net.URL;
11+
12+
public final class Api2DevdockTusStartOptionValidation {
13+
/**
14+
* Run the API2 devdock start-option validation scenario.
15+
*
16+
* @param args Unused command-line arguments.
17+
* @throws Exception Thrown when the scenario cannot be executed.
18+
*/
19+
public static void main(String[] args) throws Exception {
20+
final JSONObject scenario = Api2DevdockScenario.loadScenario();
21+
final JSONObject conformanceScenario = Api2DevdockScenario.conformanceScenario(scenario);
22+
final JSONObject result = validateStartOptions(conformanceScenario);
23+
24+
Api2DevdockScenario.writeResult(result);
25+
System.out.println(
26+
"Java TUS SDK devdock scenario "
27+
+ scenario.getString("scenarioId")
28+
+ " rejected "
29+
+ conformanceScenario.getJSONObject("completion").getString("reason")
30+
);
31+
}
32+
33+
private static JSONObject validateStartOptions(JSONObject conformanceScenario)
34+
throws Exception {
35+
final TusClient client = new TusClient();
36+
final TusStartOptions options = startOptionsFor(conformanceScenario);
37+
38+
try {
39+
client.validateStartOptions(options);
40+
} catch (IllegalArgumentException error) {
41+
final String expectedMessage = conformanceScenario
42+
.getJSONObject("completion")
43+
.getString("message");
44+
if (!expectedMessage.equals(error.getMessage())) {
45+
throw new IllegalStateException(
46+
"expected start option validation error "
47+
+ expectedMessage
48+
+ ", got "
49+
+ error.getMessage()
50+
);
51+
}
52+
53+
return new JSONObject()
54+
.put("errorCaught", true)
55+
.put("errorMessage", error.getMessage())
56+
.put("requestCount", 0);
57+
}
58+
59+
throw new IllegalStateException("start option validation scenario unexpectedly succeeded");
60+
}
61+
62+
private static TusStartOptions startOptionsFor(JSONObject conformanceScenario)
63+
throws Exception {
64+
final TusStartOptions options = new TusStartOptions();
65+
final String endpointURL = Api2DevdockScenario.conformanceInputStringOptionOrNull(
66+
conformanceScenario,
67+
"endpointUrl"
68+
);
69+
final String uploadURL = Api2DevdockScenario.conformanceInputStringOptionOrNull(
70+
conformanceScenario,
71+
"uploadUrl"
72+
);
73+
final int parallelUploads = Api2DevdockScenario.conformanceInputIntegerOption(
74+
conformanceScenario,
75+
"parallelUploads",
76+
1
77+
);
78+
final boolean uploadDataDuringCreation =
79+
Api2DevdockScenario.conformanceInputBooleanOption(
80+
conformanceScenario,
81+
"uploadDataDuringCreation",
82+
false
83+
);
84+
final boolean uploadLengthDeferred = Api2DevdockScenario.conformanceInputBooleanOption(
85+
conformanceScenario,
86+
"uploadLengthDeferred",
87+
false
88+
);
89+
90+
options.setUpload(uploadFor(conformanceScenario, uploadLengthDeferred));
91+
options.setParallelUploads(parallelUploads);
92+
options.setUploadDataDuringCreation(uploadDataDuringCreation);
93+
options.setUploadLengthDeferred(uploadLengthDeferred);
94+
if (endpointURL != null) {
95+
options.setEndpointURL(new URL(endpointURL));
96+
}
97+
if (uploadURL != null) {
98+
options.setUploadURL(new URL(uploadURL));
99+
}
100+
101+
return options;
102+
}
103+
104+
private static TusUpload uploadFor(
105+
JSONObject conformanceScenario,
106+
boolean uploadLengthDeferred
107+
) {
108+
final byte[] content = Api2DevdockScenario.conformanceInputSourceBytes(conformanceScenario);
109+
final TusUpload upload = new TusUpload();
110+
upload.setInputStream(new ByteArrayInputStream(content));
111+
upload.setSize(content.length);
112+
upload.setUploadLengthDeferred(uploadLengthDeferred);
113+
return upload;
114+
}
115+
116+
private Api2DevdockTusStartOptionValidation() {
117+
throw new IllegalStateException("Utility class");
118+
}
119+
}

src/main/java/io/tus/java/client/TusClient.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,16 @@ public int getConnectTimeout() {
225225
return connectTimeout;
226226
}
227227

228+
/**
229+
* Validate TUS start options before issuing any HTTP request.
230+
*
231+
* @param options The start options to validate.
232+
* @throws IllegalArgumentException Thrown when the options contain a known conflict.
233+
*/
234+
public void validateStartOptions(@NotNull TusStartOptions options) {
235+
TusStartOptionValidator.validate(options);
236+
}
237+
228238
/**
229239
* Create a new upload using the Creation extension. Before calling this function, an "upload
230240
* creation URL" must be defined using {@link #setUploadCreationURL(URL)} or else this

src/main/java/io/tus/java/client/TusProtocol.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,34 @@ final class TusProtocol {
3232
static final String DETAILED_ERROR_UNEXPECTED_CREATE_RESPONSE =
3333
"tus: unexpected response while creating upload";
3434
static final String DEFAULT_PROTOCOL_VERSION = "1.0.0";
35+
static final int DEFAULT_PARALLEL_UPLOADS = 1;
3536
static final Map<String, String> DEFAULT_REQUEST_HEADERS = defaultRequestHeaders();
3637
static final Map<String, String> DEFAULT_RESPONSE_HEADERS = defaultResponseHeaders();
3738
static final String LOCATION_HEADER_NAME = "Location";
3839
static final String METADATA_HEADER_NAME = "Upload-Metadata";
40+
static final int MINIMUM_PARALLEL_UPLOADS = 2;
3941
static final String OFFSET_DISCOVERY_METHOD = "HEAD";
4042
static final String REQUEST_ID_HEADER_NAME = "X-Request-ID";
43+
static final String START_OPTION_VALIDATION_MISSING_ENDPOINT_OR_UPLOAD_URL =
44+
"tus: neither an endpoint or an upload URL is provided";
45+
static final String START_OPTION_VALIDATION_MISSING_INPUT =
46+
"tus: no file or stream to upload provided";
47+
static final String START_OPTION_VALIDATION_PARALLEL_BOUNDARIES_LENGTH_MISMATCH =
48+
"tus: the `parallelUploadBoundaries` must have the same length as the value of `parallelUploads`";
49+
static final String START_OPTION_VALIDATION_PARALLEL_BOUNDARIES_WITHOUT_PARALLEL_UPLOADS =
50+
"tus: cannot use the `parallelUploadBoundaries` option when `parallelUploads` is disabled";
51+
static final String START_OPTION_VALIDATION_PARALLEL_UPLOADS_WITH_DEFERRED_LENGTH =
52+
"tus: cannot use the `uploadLengthDeferred` option when parallelUploads is enabled";
53+
static final String START_OPTION_VALIDATION_PARALLEL_UPLOADS_WITH_UPLOAD_DATA_DURING_CREATION =
54+
"tus: cannot use the `uploadDataDuringCreation` option when parallelUploads is enabled";
55+
static final String START_OPTION_VALIDATION_PARALLEL_UPLOADS_WITH_UPLOAD_SIZE =
56+
"tus: cannot use the `uploadSize` option when parallelUploads is enabled";
57+
static final String START_OPTION_VALIDATION_PARALLEL_UPLOADS_WITH_UPLOAD_URL =
58+
"tus: cannot use the `uploadUrl` option when parallelUploads is enabled";
59+
static final String START_OPTION_VALIDATION_RETRY_DELAYS_NOT_ARRAY =
60+
"tus: the `retryDelays` option must either be an array or null";
61+
static final String START_OPTION_VALIDATION_UNSUPPORTED_PROTOCOL_PREFIX =
62+
"tus: unsupported protocol ";
4163
static final int SUCCESS_RESPONSE_STATUS_CATEGORY = 200;
4264
static final String TERMINATE_UPLOAD_METHOD = "DELETE";
4365
static final String UPLOAD_BODY_CONTENT_TYPE = "application/offset+octet-stream";
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package io.tus.java.client;
2+
3+
final class TusStartOptionValidator {
4+
static void validate(TusStartOptions options) {
5+
if (missingInput(options)) {
6+
throw new IllegalArgumentException(TusProtocol.START_OPTION_VALIDATION_MISSING_INPUT);
7+
}
8+
9+
if (options.getEndpointURL() == null && options.getUploadURL() == null) {
10+
throw new IllegalArgumentException(
11+
TusProtocol.START_OPTION_VALIDATION_MISSING_ENDPOINT_OR_UPLOAD_URL
12+
);
13+
}
14+
15+
if (!TusProtocol.DEFAULT_PROTOCOL_VERSION.equals(options.getProtocol())) {
16+
throw new IllegalArgumentException(
17+
TusProtocol.START_OPTION_VALIDATION_UNSUPPORTED_PROTOCOL_PREFIX
18+
+ options.getProtocol()
19+
);
20+
}
21+
22+
final boolean parallelUploadsEnabled =
23+
options.getParallelUploads() >= TusProtocol.MINIMUM_PARALLEL_UPLOADS;
24+
final boolean parallelBoundariesSet = options.getParallelUploadBoundariesCount() >= 0;
25+
26+
if (parallelBoundariesSet && !parallelUploadsEnabled) {
27+
throw new IllegalArgumentException(
28+
TusProtocol
29+
.START_OPTION_VALIDATION_PARALLEL_BOUNDARIES_WITHOUT_PARALLEL_UPLOADS
30+
);
31+
}
32+
33+
if (!parallelUploadsEnabled) {
34+
return;
35+
}
36+
37+
if (options.getUploadURL() != null) {
38+
throw new IllegalArgumentException(
39+
TusProtocol.START_OPTION_VALIDATION_PARALLEL_UPLOADS_WITH_UPLOAD_URL
40+
);
41+
}
42+
43+
if (options.getUploadSize() != null) {
44+
throw new IllegalArgumentException(
45+
TusProtocol.START_OPTION_VALIDATION_PARALLEL_UPLOADS_WITH_UPLOAD_SIZE
46+
);
47+
}
48+
49+
if (options.isUploadLengthDeferred() || options.getUpload().isUploadLengthDeferred()) {
50+
throw new IllegalArgumentException(
51+
TusProtocol.START_OPTION_VALIDATION_PARALLEL_UPLOADS_WITH_DEFERRED_LENGTH
52+
);
53+
}
54+
55+
if (options.isUploadDataDuringCreation()) {
56+
throw new IllegalArgumentException(
57+
TusProtocol
58+
.START_OPTION_VALIDATION_PARALLEL_UPLOADS_WITH_UPLOAD_DATA_DURING_CREATION
59+
);
60+
}
61+
62+
if (parallelBoundariesSet
63+
&& options.getParallelUploadBoundariesCount() != options.getParallelUploads()) {
64+
throw new IllegalArgumentException(
65+
TusProtocol.START_OPTION_VALIDATION_PARALLEL_BOUNDARIES_LENGTH_MISMATCH
66+
);
67+
}
68+
}
69+
70+
private static boolean missingInput(TusStartOptions options) {
71+
return options == null
72+
|| options.getUpload() == null
73+
|| options.getUpload().getInputStream() == null;
74+
}
75+
76+
private TusStartOptionValidator() {
77+
throw new IllegalStateException("Utility class");
78+
}
79+
}

0 commit comments

Comments
 (0)