File: EarlyOrDelayedParsing.java

package info (click to toggle)
openjdk-21 21.0.8%2B9-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 823,976 kB
  • sloc: java: 5,613,338; xml: 1,643,607; cpp: 1,296,296; ansic: 420,291; asm: 404,850; objc: 20,994; sh: 15,271; javascript: 11,245; python: 6,895; makefile: 2,362; perl: 357; awk: 351; sed: 172; jsp: 24; csh: 3
file content (507 lines) | stat: -rw-r--r-- 19,792 bytes parent folder | download | duplicates (4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
/*
 * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

/*
 * @test
 * @bug 8293590
 * @summary URL built-in protocol handlers should parse the URL early
 *          to avoid constructing URLs for which openConnection
 *          would later throw an exception, when possible.
 *          A jdk.net.url.delayParsing property allows to switch that
 *          behavior off to mitigate risks of regression
 * @run junit  EarlyOrDelayedParsing
 * @run junit/othervm -Djdk.net.url.delayParsing EarlyOrDelayedParsing
 * @run junit/othervm -Djdk.net.url.delayParsing=true EarlyOrDelayedParsing
 * @run junit/othervm -Djdk.net.url.delayParsing=false EarlyOrDelayedParsing
 */

import java.io.IOException;
import java.net.ConnectException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

import static java.lang.System.err;
import static org.junit.jupiter.api.Assertions.*;

public class EarlyOrDelayedParsing {

    public final boolean EARLY_PARSING;
    {
        String value = System.getProperty("jdk.net.url.delayParsing", "false");
        EARLY_PARSING = !value.isEmpty() && !Boolean.parseBoolean(value);
    }

    // Some characters that when included at the wrong place
    // in the authority component, without being escaped, would
    // cause an exception.
    private static final String EXCLUDED_DELIMS = "<>\" ";
    private static final String UNWISE = "{}|\\^`";
    private static final String DELIMS = "[]/?#@";

    // Test data used to test exceptions thrown by URL
    // at some point, when constructed with some illegal input.
    sealed interface URLArgTest
            permits OneArgTest, TwoArgsTest, ThreeArgsTest, FourArgsTest {

        // Some character that is expected to cause an exception
        // at some point, and which this test case is built for
        int character();

        // An URL string containing the illegal character
        String url();

        // Some characters are already checked at construction
        // time. They will cause an exception to be thrown,
        // whether delayed parsing is activated or not.
        // This method returns true if an exception is
        // expected at construction time for this test case,
        // even when delayed parsing is activated.
        boolean early(int c);

        // The URL scheme this test case is built for.
        // Typically, one of "http", "https", "ftp"...
        default String scheme() {
            return scheme(url());
        }

        // Return the URL string of this test case, after
        // substituting its scheme with the given scheme.
        default String urlWithScheme(String scheme) {
            String url = url();
            int colon = url.indexOf(':');
            String urlWithScheme = scheme + url.substring(colon);
            return urlWithScheme;
        }

        // Which exception to expect when parsing is delayed
        default boolean acceptDelayedException(Throwable exception) {
            return exception instanceof MalformedURLException
                    || exception instanceof UnknownHostException;
        }

        default String describe() {
            return this.getClass().getSimpleName() + "(url=" + url() + ")";
        }

        static int port(String protocol) {
            return switch (protocol) {
                case "http" -> 80;
                case "https" -> 443;
                case "ftp" -> 21;
                default -> -1;
            };
        }

        static String scheme(String url) {
            return url.substring(0, url.indexOf(':'));
        }
    }

    // Test data for the one arg constructor
    // public URL(String spec) throws MalformedURLException
    sealed interface OneArgTest extends URLArgTest {

        // Create a new test case identical to this one but
        // with a different URL scheme
        default OneArgTest withScheme(String scheme) {
            String urlWithScheme = urlWithScheme(scheme);
            if (this instanceof OfHost) {
                return new OfHost(character(), urlWithScheme);
            }
            if (this instanceof OfUserInfo) {
                return new OfUserInfo(character(), urlWithScheme);
            }
            throw new AssertionError("unexpected subclass: " + this.getClass());
        }

        @Override
        default boolean early(int c) {
            return this instanceof OfHost &&
                    (c < 31 || c == 127);
        }

        @Override
        default boolean acceptDelayedException(Throwable exception) {
            return URLArgTest.super.acceptDelayedException(exception)
                    || "file".equalsIgnoreCase(scheme())
                    && character() == '\\'
                    && exception instanceof IOException;
        }

        record OfHost(int character, String url) implements OneArgTest { }
        record OfUserInfo(int character, String url) implements OneArgTest { }

        static OneArgTest ofHost(int c) {
            return new OfHost(c, "http://local%shost/".formatted(Character.toString(c)));
        }
        static OneArgTest ofUserInfo(int c) {
            return new OfUserInfo(c, "http://user%sinfo@localhost:9999/".formatted(Character.toString(c)));
        }
    }

    // Test data for the two arg constructor
    // public URL(URL context, String spec) throws MalformedURLException
    sealed interface TwoArgsTest extends URLArgTest {

        // Create a new test case identical to this one but
        // with a different URL scheme
        default TwoArgsTest withScheme(String scheme) {
            String urlWithScheme = urlWithScheme(scheme);
            if (this instanceof OfTwoArgsHost) {
                return new OfTwoArgsHost(character(), urlWithScheme);
            }
            if (this instanceof OfTwoArgsUserInfo) {
                return new OfTwoArgsUserInfo(character(), urlWithScheme);
            }
            throw new AssertionError("unexpected subclass: " + this.getClass());
        }

        @Override
        default boolean early(int c) {
            return this instanceof OfTwoArgsHost &&
                    (c < 31 || c == 127);
        }

        @Override
        default boolean acceptDelayedException(Throwable exception) {
            return URLArgTest.super.acceptDelayedException(exception)
                    || "file".equalsIgnoreCase(scheme())
                    && character() == '\\'
                    && exception instanceof IOException;
        }

        record OfTwoArgsHost(int character, String url) implements TwoArgsTest { }
        record OfTwoArgsUserInfo(int character, String url) implements TwoArgsTest { }

        static TwoArgsTest ofHost(int c) {
            return new OfTwoArgsHost(c, "http://local%shost/".formatted(Character.toString(c)));
        }
        static TwoArgsTest ofUserInfo(int c) {
            return new OfTwoArgsUserInfo(c, "http://user%sinfo@localhost:9999/".formatted(Character.toString(c)));
        }
        static TwoArgsTest ofOneArgTest(OneArgTest test) {
            if (test instanceof OneArgTest.OfHost) {
                return ofHost(test.character());
            } else if (test instanceof OneArgTest.OfUserInfo) {
                return ofUserInfo(test.character());
            }
            throw new AssertionError("can't convert to TwoArgsTest: "
                    + test.getClass());
        }
    }


    // Test data for the three args constructor
    // public URL(String scheme, String host, String file)
    //     throws MalformedURLException
    sealed interface ThreeArgsTest extends URLArgTest {

        // the host component
        String host();

        // the path + query components
        String file();

        // Create a new test case identical to this one but
        // with a different URL scheme and port
        default ThreeArgsTest withScheme(String scheme) {
            String urlWithScheme = urlWithScheme(scheme);
            if (this instanceof OfHostFile) {
                return new OfHostFile(character(), host(), file(), urlWithScheme);
            }
            throw new AssertionError("unexpected subclass: " + this.getClass());
        }

        @Override
        default boolean early(int c) {
            return (c < 31 || c == 127 || c == '/');
        }

        @Override
        default boolean acceptDelayedException(Throwable exception) {
            return URLArgTest.super.acceptDelayedException(exception)
                    || "file".equalsIgnoreCase(scheme())
                    && exception instanceof IOException;
        }

        record OfHostFile(int character, String host, String file, String url)
                implements ThreeArgsTest {
        }

        static ThreeArgsTest ofHostFile(int c) {
            String host = "local%shost".formatted(Character.toString(c));
            String url = "http://" + host + "/";
            return new OfHostFile(c, host, "/", url);
        }
    }

    // Test data for the four args constructor
    // public URL(String scheme, String host, int port, String file)
    //     throws MalformedURLException
    sealed interface FourArgsTest extends URLArgTest {

        // the host component
        String host();

        // the port component
        int port();

        // the path + query components
        String file();

        // Create a new test case identical to this one but
        // with a different URL scheme and port
        default FourArgsTest withScheme(String scheme) {
            String urlWithScheme = urlWithScheme(scheme);
            if (this instanceof OfHostFilePort) {
                int port = URLArgTest.port(scheme);
                return new OfHostFilePort(character(), host(), port, file(), urlWithScheme);
            }
            throw new AssertionError("unexpected subclass: " + this.getClass());
        }

        @Override
        default boolean early(int c) {
            return (c < 31 || c == 127 || c == '/');
        }

        @Override
        default boolean acceptDelayedException(Throwable exception) {
            return URLArgTest.super.acceptDelayedException(exception)
                    || "file".equalsIgnoreCase(scheme())
                    && exception instanceof IOException;
        }

        record OfHostFilePort(int character, String host, int port, String file, String url)
                implements FourArgsTest {
        }

        static FourArgsTest ofHostPortFile(int c) {
            String host = "local%shost".formatted(Character.toString(c));
            String url = "http://" + host + "/";
            int port = URLArgTest.port(URLArgTest.scheme(url));
            return new OfHostFilePort(c, host, port, "/", url);
        }
    }


    // Generate test data for the URL one arg constructor, with variations
    // of the host component.
    static Stream<OneArgTest> oneArgHostTests() {
        List<OneArgTest> tests = new ArrayList<>();
        List<OneArgTest> urls = new ArrayList<>();
        urls.addAll((UNWISE + EXCLUDED_DELIMS).chars()
                .mapToObj(OneArgTest::ofHost).toList());
        urls.addAll(IntStream.concat(IntStream.range(0, 31), IntStream.of(127))
                .mapToObj(OneArgTest::ofHost).toList());
        for (String scheme : List.of("http", "https", "ftp")) {
            for (var test : urls) {
                tests.add(test.withScheme(scheme));
            }
        }
        return tests.stream();
    }

    // Generate test data for the URL one arg constructor, with variations
    // of the user info component.
    static Stream<OneArgTest> oneArgUserInfoTests() {
        List<OneArgTest> tests = new ArrayList<>();
        List<OneArgTest> urls = new ArrayList<>();
        urls.addAll(IntStream.concat(IntStream.range(0, 31), IntStream.of(127))
                .mapToObj(OneArgTest::ofUserInfo).toList());
        urls.add(OneArgTest.ofUserInfo('\\'));
        for (String scheme : List.of("http", "https", "ftp")) {
            for (var test : urls) {
                tests.add(test.withScheme(scheme));
            }
        }
        return tests.stream();
    }

    // Test data with all variations for the URL one arg
    // constructor (spec)
    static Stream<OneArgTest> oneArgTests() {
        return Stream.concat(oneArgHostTests(), oneArgUserInfoTests());
    }

    // Test data with all variations for the URL two arg
    // constructor (URL, spec)
    static Stream<TwoArgsTest> twoArgTests() {
        return oneArgTests().map(TwoArgsTest::ofOneArgTest);
    }

    // Generate test data for the URL three arguments constructor
    // (scheme, host, file)
    static Stream<ThreeArgsTest> threeArgsTests() {
        List<ThreeArgsTest> urls = new ArrayList<>();
        urls.addAll((UNWISE + EXCLUDED_DELIMS + DELIMS).chars()
                .mapToObj(ThreeArgsTest::ofHostFile).toList());
        urls.addAll(IntStream.concat(IntStream.range(0, 31), IntStream.of(127))
                .mapToObj(ThreeArgsTest::ofHostFile).toList());
        List<ThreeArgsTest> tests = new ArrayList<>();
        for (String scheme : List.of("http", "https", "ftp", "file")) {
            for (var test : urls) {
                tests.add(test.withScheme(scheme));
            }
        }
        return tests.stream();
    }

    // Generate test data for the URL four arguments constructor
    // (scheme, host, port, file)
    static Stream<FourArgsTest> fourArgsTests() {
        List<FourArgsTest> urls = new ArrayList<>();
        urls.addAll((UNWISE + EXCLUDED_DELIMS + DELIMS).chars()
                .mapToObj(FourArgsTest::ofHostPortFile).toList());
        urls.addAll(IntStream.concat(IntStream.range(0, 31), IntStream.of(127))
                .mapToObj(FourArgsTest::ofHostPortFile).toList());
        List<FourArgsTest> tests = new ArrayList<>();
        for (String scheme : List.of("http", "https", "ftp", "file")) {
            for (var test : urls) {
                tests.add(test.withScheme(scheme));
            }
        }
        return tests.stream();
    }



    @ParameterizedTest
    @MethodSource("oneArgTests")
    public void testOneArgConstructor(OneArgTest test) throws Exception {

        int c = test.character();
        String url = test.url();
        if (EARLY_PARSING || test.early(c)) {
            err.println("Early parsing: " + test.describe());
            var exception = assertThrows(MalformedURLException.class, () -> {
                new URL(url);
            });
            err.println("Got expected exception: " + exception);
        } else {
            err.println("Delayed parsing: " + test.describe());
            URL u = new URL(url);
            var exception = assertThrows(IOException.class, () -> {
                u.openConnection().connect();
            });
            if (!test.acceptDelayedException(exception)) {
                    err.println("unexpected exception type: " + exception);
                    throw exception;
            }
            err.println("Got expected exception: " + exception);
            assertFalse(exception instanceof ConnectException);
        }
    }

    @ParameterizedTest
    @MethodSource("twoArgTests")
    public void testTwoArgConstructor(TwoArgsTest test) throws Exception {

        int c = test.character();
        String url = test.url();
        String scheme = URLArgTest.scheme(url);
        URL u = new URL(scheme, null,"");
        if (EARLY_PARSING || test.early(c)) {
            err.println("Early parsing: " + test.describe());
            var exception = assertThrows(MalformedURLException.class, () -> {
                new URL(u, url);
            });
            err.println("Got expected exception: " + exception);
        } else {
            err.println("Delayed parsing: " + test.describe());
            URL u2 = new URL(u, url);
            var exception = assertThrows(IOException.class, () -> {
                u2.openConnection().connect();
            });
            if (!test.acceptDelayedException(exception)) {
                err.println("unexpected exception type: " + exception);
                throw exception;
            }
            err.println("Got expected exception: " + exception);
            assertFalse(exception instanceof ConnectException);
        }
    }

    @ParameterizedTest
    @MethodSource("threeArgsTests")
    public void testThreeArgsConstructor(ThreeArgsTest test) throws Exception {

        int c = test.character();
        String url = test.url();
        if (EARLY_PARSING || test.early(c)) {
            err.println("Early parsing: " + url);
            var exception = assertThrows(MalformedURLException.class, () -> {
                new URL(test.scheme(), test.host(), test.file());
            });
            err.println("Got expected exception: " + exception);
        } else {
            err.println("Delayed parsing: " + url);
            URL u = new URL(test.scheme(), test.host(), test.file());
            var exception = assertThrows(IOException.class, () -> {
                u.openConnection().connect();
            });
            if (!test.acceptDelayedException(exception)) {
                err.println("unexpected exception type: " + exception);
                throw exception;
            }
            err.println("Got expected exception: " + exception);
            assertFalse(exception instanceof ConnectException);
        }
    }

    @ParameterizedTest
    @MethodSource("fourArgsTests")
    public void testFourArgsConstructor(FourArgsTest test) throws Exception {

        int c = test.character();
        String url = test.url();
        if (EARLY_PARSING || test.early(c)) {
            err.println("Early parsing: " + url);
            var exception = assertThrows(MalformedURLException.class, () -> {
                new URL(test.scheme(), test.host(), test.port(), test.file());
            });
            err.println("Got expected exception: " + exception);
        } else {
            err.println("Delayed parsing: " + url);
            URL u = new URL(test.scheme(), test.host(), test.port(), test.file());
            var exception = assertThrows(IOException.class, () -> {
                u.openConnection().connect();
            });
            if (!test.acceptDelayedException(exception)) {
                err.println("unexpected exception type: " + exception);
                throw exception;
            }
            err.println("Got expected exception: " + exception);
            assertFalse(exception instanceof ConnectException);
        }
    }

}