• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright (c) 2012 Jeff Ichnowski
2 // All rights reserved.
3 //
4 // Redistribution and use in source and binary forms, with or without
5 // modification, are permitted provided that the following conditions
6 // are met:
7 //
8 //     * Redistributions of source code must retain the above
9 //       copyright notice, this list of conditions and the following
10 //       disclaimer.
11 //
12 //     * Redistributions in binary form must reproduce the above
13 //       copyright notice, this list of conditions and the following
14 //       disclaimer in the documentation and/or other materials
15 //       provided with the distribution.
16 //
17 //     * Neither the name of the OWASP nor the names of its
18 //       contributors may be used to endorse or promote products
19 //       derived from this software without specific prior written
20 //       permission.
21 //
22 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
23 // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
24 // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
25 // FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
26 // COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
27 // INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
28 // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
29 // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
30 // HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
31 // STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
32 // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
33 // OF THE POSSIBILITY OF SUCH DAMAGE.
34 
35 package org.owasp.encoder;
36 
37 import java.io.CharArrayWriter;
38 import java.io.IOException;
39 import java.nio.CharBuffer;
40 import java.nio.charset.CoderResult;
41 import java.util.BitSet;
42 import junit.framework.Assert;
43 import junit.framework.Test;
44 import junit.framework.TestCase;
45 import junit.framework.TestSuite;
46 
47 /**
48  * EncoderTestSuiteBuilder -- builder of test suites for the encoders.
49  * Allows fluent construction of a test suite by specifying which
50  * code-points are not encoded, which are invalid, and the expected
51  * encodings for escaped characters.
52  *
53  * @author Jeff Ichnowski
54  */
55 public class EncoderTestSuiteBuilder {
56     /** This is the test suite that is being built. */
57     private TestSuite _suite;
58     /**
59      * If a test flagged by {@link #mark()}, this is the active suite of
60      * marked tests.
61      */
62     private TestSuite _markedSuite;
63     /** The encoder being tested. */
64     private Encoder _encoder;
65     /**
66      * A character sequence that is valid and not escaped by the encoder.
67      * It is used to surround (prefix and suffix) test inputs.
68      */
69     private String _safeAffix;
70     /**
71      * A character sequence that is escaped by the encoder.
72      */
73     private String _unsafeAffix;
74 
75     /**
76      * The set of all valid, un-escaped characters.
77      */
78     private BitSet _valid = new BitSet();
79     /**
80      * The set of all invalid characters.
81      */
82     private BitSet _invalid = new BitSet();
83     /**
84      * The set of all valid characters requiring escapes.
85      */
86     private BitSet _encoded = new BitSet();
87 
88     /**
89      * Creates an builder for the specified encoder.
90      *
91      * @param encoder the encoder to test
92      * @param safeAffix the value for {@link #_safeAffix}
93      * @param unsafeAffix the value for {@link #_unsafeAffix}
94      */
EncoderTestSuiteBuilder(Encoder encoder, String safeAffix, String unsafeAffix)95     public EncoderTestSuiteBuilder(Encoder encoder, String safeAffix, String unsafeAffix) {
96         _suite = new TestSuite(encoder.toString());
97         _encoder = encoder;
98         _safeAffix = safeAffix;
99         _unsafeAffix = unsafeAffix;
100     }
101 
102     /**
103      * Like the {@link #EncoderTestSuiteBuilder(Encoder, String, String)},
104      * but with a class that has "testXYZ()" methods in it too.  The test
105      * methods will be run first.
106      *
107      * @param suiteClass the test class with the "testXYZ()" methods.
108      * @param encoder the encoder to test
109      * @param safeAffix the value for {@link #_safeAffix}
110      * @param unsafeAffix the value for {@link #_unsafeAffix}
111      */
EncoderTestSuiteBuilder(Class<? extends TestCase> suiteClass, Encoder encoder, String safeAffix, String unsafeAffix)112     public EncoderTestSuiteBuilder(Class<? extends TestCase> suiteClass, Encoder encoder, String safeAffix, String unsafeAffix) {
113         _suite = new TestSuite(suiteClass);
114         _encoder = encoder;
115         _safeAffix = safeAffix;
116         _unsafeAffix = unsafeAffix;
117     }
118 
119     /**
120      * Adds a single test to the suite.
121      *
122      * @param test the test to add
123      * @return this.
124      */
add(Test test)125     public EncoderTestSuiteBuilder add(Test test) {
126         _suite.addTest(test);
127         return this;
128     }
129 
130     /**
131      * Java/JavaScript-style encoder for helping debug unit test results.  We
132      * should not rely upon the code being tested for helping--not that it
133      * would hurt the testing, only it might effect coverage metrics.
134      *
135      * @param input the input to encode
136      * @return the encoded input
137      */
debugEncode(String input)138     static String debugEncode(String input) {
139         final int n = input.length();
140         StringBuilder buf = new StringBuilder(n*2);
141         for (int i=0 ; i<n ; ++i) {
142             char ch = input.charAt(i);
143             switch (ch) {
144             case '\\': buf.append("\\\\"); break;
145             case '\'': buf.append("\\\'"); break;
146             case '\"': buf.append("\\\""); break;
147             case '\r': buf.append("\\r"); break;
148             case '\n': buf.append("\\n"); break;
149             case '\t': buf.append("\\t"); break;
150             default:
151                 if (' ' <= ch && ch <= '~') {
152                     buf.append(ch);
153                 } else {
154                     buf.append(String.format("\\u%04x", (int) ch));
155                 }
156                 break;
157             }
158         }
159         return buf.toString();
160     }
161 
162     /**
163      * Extended version of {@link junit.framework.Assert#assertEquals(String,String)}.
164      * It will try encodings by surrounding the input with safe and unsafe
165      * prefixes and suffixes.  It will also check additional assertions about
166      * the behavior of the encoders.
167      *
168      * @param expected the expected encoding
169      * @param input the input to encode
170      * @throws IOException from the signature of a Writer used internally.
171      * Not actually thrown since the writer is an in-memory writer.
172      */
checkEncode(final String expected, final String input)173     void checkEncode(final String expected, final String input)
174         throws IOException
175     {
176         // Check the .encode call
177         String actual = Encode.encode(_encoder, input);
178 
179         if (!expected.equals(actual)) {
180             Assert.assertEquals("encode(\""+ debugEncode(input) +"\")", expected, actual);
181         }
182 
183         if (input.equals(actual)) {
184             // test that the input string is returned unmodified if
185             // the input was not escaped.  This insures that we're
186             // not allocating objects unnecessarily.
187             Assert.assertSame(input, actual);
188         }
189 
190         // Check the encodeTo variants (at offset 0)
191         TestWriter testWriter = new TestWriter(input);
192         EncodedWriter encodedWriter = new EncodedWriter(testWriter, _encoder);
193         encodedWriter.write(input);
194         encodedWriter.close();
195         actual = testWriter.toString();
196         if (!expected.equals(actual)) {
197             Assert.assertEquals("encodeTo(\""+debugEncode(input)+"\",int,int,Writer)", expected, actual);
198         }
199 
200         // Check the encodeTo variants (at offset 3)
201         String offsetInput = "\0\0\0" + input + "\0\0\0";
202         testWriter = new TestWriter(offsetInput);
203         encodedWriter = new EncodedWriter(testWriter, _encoder);
204         encodedWriter.write(offsetInput.toCharArray(), 3, input.length());
205         encodedWriter.close();
206         actual = testWriter.toString();
207         if (!expected.equals(actual)) {
208             Assert.assertEquals("encodeTo([..."+debugEncode(input)+"...],int,int,Writer)", expected, actual);
209         }
210 
211         // Check boundary conditions on CharBuffer encodes
212         checkBoundaryEncodes(expected, input);
213     }
214 
215     /**
216      * Checks boundary conditions of CharBuffer based encodes.
217      *
218      * @param expected the expected output
219      * @param input the input to encode
220      */
checkBoundaryEncodes(String expected, String input)221     private void checkBoundaryEncodes(String expected, String input) {
222         final CharBuffer in = CharBuffer.wrap(input.toCharArray());
223         final int n = expected.length();
224         final CharBuffer out = CharBuffer.allocate(n);
225         for (int i=0 ; i<n ; ++i) {
226             out.clear();
227             out.position(n - i);
228             in.clear();
229 
230             CoderResult cr = _encoder.encode(in, out, true);
231             out.limit(out.position()).position(n - i);
232             out.compact();
233             if (cr.isOverflow()) {
234                 CoderResult cr2 = _encoder.encode(in, out, true);
235                 if (!cr2.isUnderflow()) {
236                     Assert.fail("second encode should finish at offset = "+i);
237                 }
238             }
239             out.flip();
240 
241             String actual = out.toString();
242             if (!expected.equals(actual)) {
243                 Assert.assertEquals("offset = "+i, expected, actual);
244             }
245         }
246     }
247 
248     /**
249      * Tells the suite builder that for the given input it should expect the
250      * given encoded output.  To be used only for input that is escaped by
251      * the encoder.
252      *
253      * @param expected the expected output.
254      * @param input the input to encode.
255      * @return this.
256      */
encode(final String expected, final String input)257     public EncoderTestSuiteBuilder encode(final String expected, final String input) {
258         return encode("input: "+input, expected, input);
259     }
260 
261     /**
262      * Tells the suite builder that for the given input it should expect the
263      * given encoded output.  To be used only for input that is escaped by
264      * the encoder.
265      *
266      * @param name the name of the test (for junit reports)
267      * @param expected the expected output
268      * @param input the input to encode.
269      * @return this.
270      */
encode(String name, final String expected, final String input)271     public EncoderTestSuiteBuilder encode(String name, final String expected, final String input) {
272         return add(new TestCase(name) {
273             @Override
274             protected void runTest() throws Throwable {
275                 // test input directly
276                 checkEncode(expected, input);
277 
278                 // test input surrounded by safe characters
279                 checkEncode(_safeAffix + expected, _safeAffix + input);
280                 checkEncode(expected + _safeAffix, input + _safeAffix);
281                 checkEncode(_safeAffix + expected + _safeAffix,
282                     _safeAffix + input + _safeAffix);
283 
284                 // test input surrounded by characters needing escape
285                 String escapedAffix = Encode.encode(_encoder, _unsafeAffix);
286                 checkEncode(escapedAffix + expected, _unsafeAffix + input);
287                 checkEncode(expected + escapedAffix, input + _unsafeAffix);
288                 checkEncode(escapedAffix + expected + escapedAffix,
289                     _unsafeAffix + input + _unsafeAffix);
290             }
291         });
292     }
293 
294     /**
295      * Tells the builder that any character in the input string is "invalid"--
296      * and thus is not to appear in the output either encoded or unescaped.
297      *
298      * @param chars the set of invalid characters.
299      */
300     public void invalid(String chars) {
301         for (int i=0, n=chars.length() ; i<n ; ++i) {
302             char ch = chars.charAt(i);
303             _invalid.set(ch);
304             _valid.clear(ch);
305             _encoded.clear(ch);
306         }
307     }
308 
309     /**
310      * Tells the builder that a character range is invalid.
311      *
312      * @param min the minimum code-point (inclusive)
313      * @param max the maximum code-point (inclusive)
314      * @return this.
315      */
316     public EncoderTestSuiteBuilder invalid(int min, int max) {
317         _invalid.set(min, max+1);
318         _valid.clear(min, max+1);
319         _encoded.clear(min, max+1);
320         return this;
321     }
322 
323     /**
324      * Tells the builder that a character set is valid and unescaped.
325      *
326      * @param chars the character set of valid, unescaped characters.
327      * @return this.
328      */
329     public EncoderTestSuiteBuilder valid(String chars) {
330         for (int i=0, n=chars.length() ; i<n ; ++i) {
331             char ch = chars.charAt(i);
332             _valid.set(ch);
333             _invalid.clear(ch);
334             _encoded.clear(ch);
335         }
336         return this;
337     }
338 
339     /**
340      * Tells the builder that a range of code-points is valid.
341      *
342      * @param min the minimum (inclusive)
343      * @param max the maximum (inclusive)
344      * @return this.
345      */
346     public EncoderTestSuiteBuilder valid(int min, int max) {
347         _valid.set(min, max+1);
348         _invalid.clear(min, max+1);
349         _encoded.clear(min, max+1);
350         return this;
351     }
352 
353     /**
354      * Tells the builder that a set of characters is encoded.
355      *
356      * @param chars the encoded characters
357      * @return this
358      */
359     public EncoderTestSuiteBuilder encoded(String chars) {
360         for (int i=0, n=chars.length() ; i<n ; ++i) {
361             char ch = chars.charAt(i);
362             _encoded.set(ch);
363             _valid.clear(ch);
364             _invalid.clear(ch);
365         }
366         return this;
367     }
368 
369     /**
370      * Tells the builder that a range of characters is encoded.
371      *
372      * @param min the minimum (inclusive)
373      * @param max the maximum (inclusive)
374      * @return this
375      */
376     public EncoderTestSuiteBuilder encoded(int min, int max) {
377         _encoded.set(min, max+1);
378         _valid.clear(min, max+1);
379         _invalid.clear(min, max+1);
380         return this;
381     }
382 
383     /**
384      * Creates and adds a test suite of valid, unescaped characters, to
385      * the test suite.  Must be called after telling the builder which
386      * characters are valid, invalid, and encoded.
387      *
388      * @return this.
389      */
390     public EncoderTestSuiteBuilder validSuite() {
391         int cardinality = _encoded.cardinality() + _invalid.cardinality() + _valid.cardinality();
392         if (cardinality != Character.MAX_CODE_POINT + 1) {
393             throw new AssertionError("incomplete coverage: "+cardinality+" != "+(Character.MAX_CODE_POINT+1));
394         }
395 
396         TestSuite suite = new TestSuite("valid");
397 
398         int min = _valid.nextSetBit(0);
399         while (min != -1) {
400             int max = _valid.nextClearBit(min+1);
401             if (max == -1) {
402                 max = Character.MAX_CODE_POINT + 1;
403             }
404             final int finalMin = min;
405             final int finalMax = max;
406             suite.addTest(new TestCase(String.format("U+%04X..U+%04X", finalMin, finalMax - 1)) {
407                 @Override
408                 protected void runTest() throws Throwable {
409                     char[] chars = new char[2];
410                     for (int i= finalMin; i<finalMax; ++i) {
411                         String input = new String(chars, 0, Character.toChars(i, chars, 0));
412                         checkEncode(input, input);
413                     }
414                 }
415             });
416             min = _valid.nextSetBit(max+1);
417         }
418 
419         return add(suite);
420     }
421 
422     /**
423      * Creates an adds a test suite for the invalid characters.  Must be
424      * called after telling the builder which characters are valid, invalid,
425      * and encoded.
426      *
427      * @param invalidChar the replacement to expect when an invalid character
428      * is encountered in the input.
429      * @return this
430      */
431     public EncoderTestSuiteBuilder invalidSuite(char invalidChar) {
432         assert _encoded.cardinality() + _invalid.cardinality() + _valid.cardinality() == Character.MAX_CODE_POINT + 1;
433 
434         final String invalidString = String.valueOf(invalidChar);
435 
436         TestSuite suite = new TestSuite("invalid");
437         int min = _invalid.nextSetBit(0);
438         while (min != -1) {
439             int max = _invalid.nextClearBit(min+1);
440             if (max < 0) {
441                 max = Character.MAX_CODE_POINT + 1;
442             }
443             final int finalMin = min;
444             final int finalMax = max;
445             suite.addTest(new TestCase(String.format("U+%04x..U+%04X", finalMin, finalMax - 1)) {
446                 @Override
447                 protected void runTest() throws Throwable {
448                     char[] chars = new char[2];
449                     for (int i = finalMin; i < finalMax; ++i) {
450                         String input = new String(chars, 0, Character.toChars(i, chars, 0));
451                         String actual = Encode.encode(_encoder, input);
452                         if (!invalidString.equals(actual)) {
453                             assertEquals("\"" + debugEncode(input) + "\" actual=" + debugEncode(actual), invalidString, actual);
454                         }
455                         checkBoundaryEncodes(invalidString, input);
456                     }
457                 }
458             });
459             min = _invalid.nextSetBit(max+1);
460         }
461 
462         return add(suite);
463     }
464 
465     /**
466      * Creates and adds a test suite for characters that are encoded.  Must
467      * be called after telling the builder which characters are valid,
468      * invalid, and encoded.  The added suite simply tests that encoded
469      * characters are encoded to something other than the input.  The
470      * {@link #encode(String, String)} methods should be use to test
471      * actual encoded return values.
472      *
473      * @see #encode(String, String)
474      * @see #encode(String, String, String)
475      * @return this
476      */
477     public EncoderTestSuiteBuilder encodedSuite() {
478         assert _encoded.cardinality() + _invalid.cardinality() + _valid.cardinality() == Character.MAX_CODE_POINT + 1;
479 
480         TestSuite suite = new TestSuite("encoded");
481         int min = _encoded.nextSetBit(0);
482         while (min != -1) {
483             int max = _encoded.nextClearBit(min+1);
484             if (max < 0) {
485                 max = Character.MAX_CODE_POINT+1;
486             }
487             final int finalMin = min;
488             final int finalMax = max;
489             suite.addTest(new TestCase(String.format("U+%04X..U+%04X", finalMin, finalMax - 1)) {
490                 @Override
491                 protected void runTest() throws Throwable {
492                     char[] chars = new char[2];
493                     for (int i= finalMin; i< finalMax; ++i) {
494                         String input = new String(chars, 0, Character.toChars(i, chars, 0));
495                         String actual = Encode.encode(_encoder, input);
496                         if (actual.equals(input)) {
497                             fail("input="+debugEncode(input));
498                         }
499                     }
500                 }
501             });
502             min = _encoded.nextSetBit(max+1);
503         }
504         return add(suite);
505     }
506 
507     /**
508      * Returns the suite that was built.  If any tests were flagged with
509      * {@link #mark()}, then only those tests will be included in the result.
510      *
511      * @see #mark()
512      * @return the test suite.
513      */
514     public TestSuite build() {
515         return _markedSuite != null ? _markedSuite : _suite;
516     }
517 
518     /**
519      * "Marks" the last added test for running.  If any tests in the suite is
520      * marked, then only the marked tests are run.  Marking allows developers
521      * to flag a single test during development and may be useful for debugging
522      * or simplying focusing on a perticular (set of) test(s).
523      *
524      * @return this
525      */
526     public EncoderTestSuiteBuilder mark() {
527         if (_markedSuite == null) {
528             _markedSuite = new TestSuite();
529         }
530         _markedSuite.addTest(_suite.testAt(_suite.testCount() - 1));
531         return this;
532     }
533 
534     /**
535      * A writer used during testing.  It extends CharArrayWriter to
536      * add assertions to the test sequence, while also buffering the
537      * result for later assertions.
538      */
539     static class TestWriter extends CharArrayWriter {
540         private String _input;
541 
542         public TestWriter(String input) {
543             _input = input;
544         }
545 
546         @Override
547         public void write(String str) throws IOException {
548             // Make sure that if the write(String...) apis are called, that its
549             // only for the case that the input is unchanged.
550             Assert.assertSame(_input, str);
551             super.write(str);
552         }
553 
554         @Override
555         public void write(String str, int off, int len) {
556             // Make sure that if the write(String...) apis are called, that its
557             // only for the case that the input is unchanged.
558             Assert.assertSame(_input, str);
559             super.write(str, off, len);
560         }
561     }
562 }
563