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