1/* 2 * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. 8 * 9 * This code is distributed in the hope that it will be useful, but WITHOUT 10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 12 * version 2 for more details (a copy is included in the LICENSE file that 13 * accompanied this code). 14 * 15 * You should have received a copy of the GNU General Public License version 16 * 2 along with this work; if not, write to the Free Software Foundation, 17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 18 * 19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 20 * or visit www.oracle.com if you need additional information or have any 21 * questions. 22 */ 23 24import java.io.ByteArrayOutputStream; 25import java.io.FilterInputStream; 26import java.io.IOException; 27import java.io.InputStream; 28import java.io.OutputStream; 29import java.io.Serializable; 30import java.net.InetAddress; 31import java.net.ServerSocket; 32import java.net.Socket; 33import java.net.SocketAddress; 34import java.net.SocketException; 35import java.net.SocketOption; 36import java.nio.channels.ServerSocketChannel; 37import java.nio.channels.SocketChannel; 38import java.rmi.server.RMIClientSocketFactory; 39import java.rmi.server.RMIServerSocketFactory; 40import java.rmi.server.RMISocketFactory; 41import java.util.ArrayList; 42import java.util.Arrays; 43import java.util.List; 44import java.util.Objects; 45import java.util.Set; 46 47import org.testng.Assert; 48import org.testng.annotations.Test; 49import org.testng.annotations.DataProvider; 50 51/* 52 * @test 53 * @summary TestSocket Factory and tests of the basic trigger, match, and replace functions 54 * @run testng TestSocketFactory 55 * @bug 8186539 56 */ 57 58/** 59 * A RMISocketFactory utility factory to log RMI stream contents and to 60 * trigger, and then match and replace output stream contents to simulate failures. 61 * <p> 62 * The trigger is a sequence of bytes that must be found before looking 63 * for the bytes to match and replace. If the trigger sequence is empty 64 * matching is immediately enabled. While waiting for the trigger to be found 65 * bytes written to the streams are written through to the output stream. 66 * The when triggered and when a trigger is non-empty, matching looks for 67 * the sequence of bytes supplied. If the sequence is empty, no matching or 68 * replacement is performed. 69 * While waiting for a complete match, the partial matched bytes are not 70 * written to the output stream. When the match is incomplete, the partial 71 * matched bytes are written to the output. When a match is complete the 72 * full replacement byte array is written to the output. 73 * <p> 74 * The trigger, match, and replacement bytes arrays can be changed at any 75 * time and immediately reset and restart matching. Changes are propagated 76 * to all of the sockets created from the factories immediately. 77 */ 78public class TestSocketFactory extends RMISocketFactory 79 implements RMIClientSocketFactory, RMIServerSocketFactory, Serializable { 80 81 private static final long serialVersionUID = 1L; 82 83 private volatile transient byte[] triggerBytes; 84 85 private volatile transient byte[] matchBytes; 86 87 private volatile transient byte[] replaceBytes; 88 89 private transient final List<InterposeSocket> sockets = new ArrayList<>(); 90 91 private transient final List<InterposeServerSocket> serverSockets = new ArrayList<>(); 92 93 static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; 94 95 public static final boolean DEBUG = false; 96 97 /** 98 * Debugging output can be synchronized with logging of RMI actions. 99 * 100 * @param format a printf format 101 * @param args any args 102 */ 103 private static void DEBUG(String format, Object... args) { 104 if (DEBUG) { 105 System.err.printf(format, args); 106 } 107 } 108 109 /** 110 * Create a socket factory that creates InputStreams 111 * and OutputStreams that log. 112 */ 113 public TestSocketFactory() { 114 this.triggerBytes = EMPTY_BYTE_ARRAY; 115 this.matchBytes = EMPTY_BYTE_ARRAY; 116 this.replaceBytes = EMPTY_BYTE_ARRAY; 117 } 118 119 /** 120 * Set the match and replacement bytes, with an empty trigger. 121 * The match and replacements are propagated to all existing sockets. 122 * 123 * @param matchBytes bytes to match 124 * @param replaceBytes bytes to replace the matched bytes 125 */ 126 public void setMatchReplaceBytes(byte[] matchBytes, byte[] replaceBytes) { 127 setMatchReplaceBytes(EMPTY_BYTE_ARRAY, matchBytes, replaceBytes); 128 } 129 130 /** 131 * Set the trigger, match, and replacement bytes. 132 * The trigger, match, and replacements are propagated to all existing sockets. 133 * 134 * @param triggerBytes array of bytes to use as a trigger, may be zero length 135 * @param matchBytes bytes to match after the trigger has been seen 136 * @param replaceBytes bytes to replace the matched bytes 137 */ 138 public void setMatchReplaceBytes(byte[] triggerBytes, byte[] matchBytes, 139 byte[] replaceBytes) { 140 this.triggerBytes = Objects.requireNonNull(triggerBytes, "triggerBytes"); 141 this.matchBytes = Objects.requireNonNull(matchBytes, "matchBytes"); 142 this.replaceBytes = Objects.requireNonNull(replaceBytes, "replaceBytes"); 143 sockets.forEach( s -> s.setMatchReplaceBytes(triggerBytes, matchBytes, 144 replaceBytes)); 145 serverSockets.forEach( s -> s.setMatchReplaceBytes(triggerBytes, matchBytes, 146 replaceBytes)); 147 } 148 149 @Override 150 public Socket createSocket(String host, int port) throws IOException { 151 Socket socket = RMISocketFactory.getDefaultSocketFactory() 152 .createSocket(host, port); 153 InterposeSocket s = new InterposeSocket(socket, 154 triggerBytes, matchBytes, replaceBytes); 155 sockets.add(s); 156 return s; 157 } 158 159 /** 160 * Return the current list of sockets. 161 * @return Return a snapshot of the current list of sockets 162 */ 163 public List<InterposeSocket> getSockets() { 164 List<InterposeSocket> snap = new ArrayList<>(sockets); 165 return snap; 166 } 167 168 @Override 169 public ServerSocket createServerSocket(int port) throws IOException { 170 171 ServerSocket serverSocket = RMISocketFactory.getDefaultSocketFactory() 172 .createServerSocket(port); 173 InterposeServerSocket ss = new InterposeServerSocket(serverSocket, 174 triggerBytes, matchBytes, replaceBytes); 175 serverSockets.add(ss); 176 return ss; 177 } 178 179 /** 180 * Return the current list of server sockets. 181 * @return Return a snapshot of the current list of server sockets 182 */ 183 public List<InterposeServerSocket> getServerSockets() { 184 List<InterposeServerSocket> snap = new ArrayList<>(serverSockets); 185 return snap; 186 } 187 188 /** 189 * An InterposeSocket wraps a socket that produces InputStreams 190 * and OutputStreams that log the traffic. 191 * The OutputStreams it produces watch for a trigger and then 192 * match an array of bytes and replace them. 193 * Useful for injecting protocol and content errors. 194 */ 195 public static class InterposeSocket extends Socket { 196 private final Socket socket; 197 private InputStream in; 198 private MatchReplaceOutputStream out; 199 private volatile byte[] triggerBytes; 200 private volatile byte[] matchBytes; 201 private volatile byte[] replaceBytes; 202 private final ByteArrayOutputStream inLogStream; 203 private final ByteArrayOutputStream outLogStream; 204 private final String name; 205 private static volatile int num = 0; // index for created InterposeSockets 206 207 /** 208 * Construct a socket that interposes on a socket to match and replace. 209 * The trigger is empty. 210 * @param socket the underlying socket 211 * @param matchBytes the bytes that must match 212 * @param replaceBytes the replacement bytes 213 */ 214 public InterposeSocket(Socket socket, byte[] matchBytes, byte[] replaceBytes) { 215 this(socket, EMPTY_BYTE_ARRAY, matchBytes, replaceBytes); 216 } 217 218 /** 219 * Construct a socket that interposes on a socket to match and replace. 220 * @param socket the underlying socket 221 * @param triggerBytes array of bytes to enable matching 222 * @param matchBytes the bytes that must match 223 * @param replaceBytes the replacement bytes 224 */ 225 public InterposeSocket(Socket socket, byte[] 226 triggerBytes, byte[] matchBytes, byte[] replaceBytes) { 227 this.socket = socket; 228 this.triggerBytes = Objects.requireNonNull(triggerBytes, "triggerBytes"); 229 this.matchBytes = Objects.requireNonNull(matchBytes, "matchBytes"); 230 this.replaceBytes = Objects.requireNonNull(replaceBytes, "replaceBytes"); 231 this.inLogStream = new ByteArrayOutputStream(); 232 this.outLogStream = new ByteArrayOutputStream(); 233 this.name = "IS" + ++num + "::" 234 + Thread.currentThread().getName() + ": " 235 + socket.getLocalPort() + " < " + socket.getPort(); 236 } 237 238 /** 239 * Set the match and replacement bytes, with an empty trigger. 240 * The match and replacements are propagated to all existing sockets. 241 * 242 * @param matchBytes bytes to match 243 * @param replaceBytes bytes to replace the matched bytes 244 */ 245 public void setMatchReplaceBytes(byte[] matchBytes, byte[] replaceBytes) { 246 this.setMatchReplaceBytes(EMPTY_BYTE_ARRAY, matchBytes, replaceBytes); 247 } 248 249 /** 250 * Set the trigger, match, and replacement bytes. 251 * The trigger, match, and replacements are propagated to the 252 * MatchReplaceOutputStream. 253 * 254 * @param triggerBytes array of bytes to use as a trigger, may be zero length 255 * @param matchBytes bytes to match after the trigger has been seen 256 * @param replaceBytes bytes to replace the matched bytes 257 */ 258 public void setMatchReplaceBytes(byte[] triggerBytes, byte[] matchBytes, 259 byte[] replaceBytes) { 260 this.triggerBytes = triggerBytes; 261 this.matchBytes = matchBytes; 262 this.replaceBytes = replaceBytes; 263 out.setMatchReplaceBytes(triggerBytes, matchBytes, replaceBytes); 264 } 265 266 @Override 267 public void connect(SocketAddress endpoint) throws IOException { 268 socket.connect(endpoint); 269 } 270 271 @Override 272 public void connect(SocketAddress endpoint, int timeout) throws IOException { 273 socket.connect(endpoint, timeout); 274 } 275 276 @Override 277 public void bind(SocketAddress bindpoint) throws IOException { 278 socket.bind(bindpoint); 279 } 280 281 @Override 282 public InetAddress getInetAddress() { 283 return socket.getInetAddress(); 284 } 285 286 @Override 287 public InetAddress getLocalAddress() { 288 return socket.getLocalAddress(); 289 } 290 291 @Override 292 public int getPort() { 293 return socket.getPort(); 294 } 295 296 @Override 297 public int getLocalPort() { 298 return socket.getLocalPort(); 299 } 300 301 @Override 302 public SocketAddress getRemoteSocketAddress() { 303 return socket.getRemoteSocketAddress(); 304 } 305 306 @Override 307 public SocketAddress getLocalSocketAddress() { 308 return socket.getLocalSocketAddress(); 309 } 310 311 @Override 312 public SocketChannel getChannel() { 313 return socket.getChannel(); 314 } 315 316 @Override 317 public synchronized void close() throws IOException { 318 socket.close(); 319 } 320 321 @Override 322 public String toString() { 323 return "InterposeSocket " + name + ": " + socket.toString(); 324 } 325 326 @Override 327 public boolean isConnected() { 328 return socket.isConnected(); 329 } 330 331 @Override 332 public boolean isBound() { 333 return socket.isBound(); 334 } 335 336 @Override 337 public boolean isClosed() { 338 return socket.isClosed(); 339 } 340 341 @Override 342 public <T> Socket setOption(SocketOption<T> name, T value) throws IOException { 343 return socket.setOption(name, value); 344 } 345 346 @Override 347 public <T> T getOption(SocketOption<T> name) throws IOException { 348 return socket.getOption(name); 349 } 350 351 @Override 352 public Set<SocketOption<?>> supportedOptions() { 353 return socket.supportedOptions(); 354 } 355 356 @Override 357 public synchronized InputStream getInputStream() throws IOException { 358 if (in == null) { 359 in = socket.getInputStream(); 360 String name = Thread.currentThread().getName() + ": " 361 + socket.getLocalPort() + " < " + socket.getPort(); 362 in = new LoggingInputStream(in, name, inLogStream); 363 DEBUG("Created new InterposeInputStream: %s%n", name); 364 } 365 return in; 366 } 367 368 @Override 369 public synchronized OutputStream getOutputStream() throws IOException { 370 if (out == null) { 371 OutputStream o = socket.getOutputStream(); 372 String name = Thread.currentThread().getName() + ": " 373 + socket.getLocalPort() + " > " + socket.getPort(); 374 out = new MatchReplaceOutputStream(o, name, outLogStream, 375 triggerBytes, matchBytes, replaceBytes); 376 DEBUG("Created new MatchReplaceOutputStream: %s%n", name); 377 } 378 return out; 379 } 380 381 /** 382 * Return the bytes logged from the input stream. 383 * @return Return the bytes logged from the input stream. 384 */ 385 public byte[] getInLogBytes() { 386 return inLogStream.toByteArray(); 387 } 388 389 /** 390 * Return the bytes logged from the output stream. 391 * @return Return the bytes logged from the output stream. 392 */ 393 public byte[] getOutLogBytes() { 394 return outLogStream.toByteArray(); 395 } 396 397 } 398 399 /** 400 * InterposeServerSocket is a ServerSocket that wraps each Socket it accepts 401 * with an InterposeSocket so that its input and output streams can be monitored. 402 */ 403 public static class InterposeServerSocket extends ServerSocket { 404 private final ServerSocket socket; 405 private volatile byte[] triggerBytes; 406 private volatile byte[] matchBytes; 407 private volatile byte[] replaceBytes; 408 private final List<InterposeSocket> sockets = new ArrayList<>(); 409 410 /** 411 * Construct a server socket that interposes on a socket to match and replace. 412 * The trigger is empty. 413 * @param socket the underlying socket 414 * @param matchBytes the bytes that must match 415 * @param replaceBytes the replacement bytes 416 */ 417 public InterposeServerSocket(ServerSocket socket, byte[] matchBytes, 418 byte[] replaceBytes) throws IOException { 419 this(socket, EMPTY_BYTE_ARRAY, matchBytes, replaceBytes); 420 } 421 422 /** 423 * Construct a server socket that interposes on a socket to match and replace. 424 * @param socket the underlying socket 425 * @param triggerBytes array of bytes to enable matching 426 * @param matchBytes the bytes that must match 427 * @param replaceBytes the replacement bytes 428 */ 429 public InterposeServerSocket(ServerSocket socket, byte[] triggerBytes, 430 byte[] matchBytes, byte[] replaceBytes) throws IOException { 431 this.socket = socket; 432 this.triggerBytes = Objects.requireNonNull(triggerBytes, "triggerBytes"); 433 this.matchBytes = Objects.requireNonNull(matchBytes, "matchBytes"); 434 this.replaceBytes = Objects.requireNonNull(replaceBytes, "replaceBytes"); 435 } 436 437 /** 438 * Set the match and replacement bytes, with an empty trigger. 439 * The match and replacements are propagated to all existing sockets. 440 * 441 * @param matchBytes bytes to match 442 * @param replaceBytes bytes to replace the matched bytes 443 */ 444 public void setMatchReplaceBytes(byte[] matchBytes, byte[] replaceBytes) { 445 setMatchReplaceBytes(EMPTY_BYTE_ARRAY, matchBytes, replaceBytes); 446 } 447 448 /** 449 * Set the trigger, match, and replacement bytes. 450 * The trigger, match, and replacements are propagated to all existing sockets. 451 * 452 * @param triggerBytes array of bytes to use as a trigger, may be zero length 453 * @param matchBytes bytes to match after the trigger has been seen 454 * @param replaceBytes bytes to replace the matched bytes 455 */ 456 public void setMatchReplaceBytes(byte[] triggerBytes, byte[] matchBytes, 457 byte[] replaceBytes) { 458 this.triggerBytes = triggerBytes; 459 this.matchBytes = matchBytes; 460 this.replaceBytes = replaceBytes; 461 sockets.forEach(s -> s.setMatchReplaceBytes(triggerBytes, matchBytes, replaceBytes)); 462 } 463 /** 464 * Return a snapshot of the current list of sockets created from this server socket. 465 * @return Return a snapshot of the current list of sockets 466 */ 467 public List<InterposeSocket> getSockets() { 468 List<InterposeSocket> snap = new ArrayList<>(sockets); 469 return snap; 470 } 471 472 @Override 473 public void bind(SocketAddress endpoint) throws IOException { 474 socket.bind(endpoint); 475 } 476 477 @Override 478 public void bind(SocketAddress endpoint, int backlog) throws IOException { 479 socket.bind(endpoint, backlog); 480 } 481 482 @Override 483 public InetAddress getInetAddress() { 484 return socket.getInetAddress(); 485 } 486 487 @Override 488 public int getLocalPort() { 489 return socket.getLocalPort(); 490 } 491 492 @Override 493 public SocketAddress getLocalSocketAddress() { 494 return socket.getLocalSocketAddress(); 495 } 496 497 @Override 498 public Socket accept() throws IOException { 499 Socket s = socket.accept(); 500 InterposeSocket socket = new InterposeSocket(s, matchBytes, replaceBytes); 501 sockets.add(socket); 502 return socket; 503 } 504 505 @Override 506 public void close() throws IOException { 507 socket.close(); 508 } 509 510 @Override 511 public ServerSocketChannel getChannel() { 512 return socket.getChannel(); 513 } 514 515 @Override 516 public boolean isClosed() { 517 return socket.isClosed(); 518 } 519 520 @Override 521 public String toString() { 522 return socket.toString(); 523 } 524 525 @Override 526 public <T> ServerSocket setOption(SocketOption<T> name, T value) 527 throws IOException { 528 return socket.setOption(name, value); 529 } 530 531 @Override 532 public <T> T getOption(SocketOption<T> name) throws IOException { 533 return socket.getOption(name); 534 } 535 536 @Override 537 public Set<SocketOption<?>> supportedOptions() { 538 return socket.supportedOptions(); 539 } 540 541 @Override 542 public synchronized void setSoTimeout(int timeout) throws SocketException { 543 socket.setSoTimeout(timeout); 544 } 545 546 @Override 547 public synchronized int getSoTimeout() throws IOException { 548 return socket.getSoTimeout(); 549 } 550 } 551 552 /** 553 * LoggingInputStream is a stream and logs all bytes read to it. 554 * For identification it is given a name. 555 */ 556 public static class LoggingInputStream extends FilterInputStream { 557 private int bytesIn = 0; 558 private final String name; 559 private final OutputStream log; 560 561 public LoggingInputStream(InputStream in, String name, OutputStream log) { 562 super(in); 563 this.name = name; 564 this.log = log; 565 } 566 567 @Override 568 public int read() throws IOException { 569 int b = super.read(); 570 if (b >= 0) { 571 log.write(b); 572 bytesIn++; 573 } 574 return b; 575 } 576 577 @Override 578 public int read(byte[] b, int off, int len) throws IOException { 579 int bytes = super.read(b, off, len); 580 if (bytes > 0) { 581 log.write(b, off, bytes); 582 bytesIn += bytes; 583 } 584 return bytes; 585 } 586 587 @Override 588 public int read(byte[] b) throws IOException { 589 return read(b, 0, b.length); 590 } 591 592 @Override 593 public void close() throws IOException { 594 super.close(); 595 } 596 597 @Override 598 public String toString() { 599 return String.format("%s: In: (%d)", name, bytesIn); 600 } 601 } 602 603 /** 604 * An OutputStream that looks for a trigger to enable matching and 605 * replaces one string of bytes with another. 606 * If any range matches, the match starts after the partial match. 607 */ 608 static class MatchReplaceOutputStream extends OutputStream { 609 private final OutputStream out; 610 private final String name; 611 private volatile byte[] triggerBytes; 612 private volatile byte[] matchBytes; 613 private volatile byte[] replaceBytes; 614 int triggerIndex; 615 int matchIndex; 616 private int bytesOut = 0; 617 private final OutputStream log; 618 619 MatchReplaceOutputStream(OutputStream out, String name, OutputStream log, 620 byte[] matchBytes, byte[] replaceBytes) { 621 this(out, name, log, EMPTY_BYTE_ARRAY, matchBytes, replaceBytes); 622 } 623 624 MatchReplaceOutputStream(OutputStream out, String name, OutputStream log, 625 byte[] triggerBytes, byte[] matchBytes, 626 byte[] replaceBytes) { 627 this.out = out; 628 this.name = name; 629 this.triggerBytes = Objects.requireNonNull(triggerBytes, "triggerBytes"); 630 triggerIndex = 0; 631 this.matchBytes = Objects.requireNonNull(matchBytes, "matchBytes"); 632 this.replaceBytes = Objects.requireNonNull(replaceBytes, "replaceBytes"); 633 matchIndex = 0; 634 this.log = log; 635 } 636 637 public void setMatchReplaceBytes(byte[] matchBytes, byte[] replaceBytes) { 638 setMatchReplaceBytes(EMPTY_BYTE_ARRAY, matchBytes, replaceBytes); 639 } 640 641 public void setMatchReplaceBytes(byte[] triggerBytes, byte[] matchBytes, 642 byte[] replaceBytes) { 643 this.triggerBytes = Objects.requireNonNull(triggerBytes, "triggerBytes"); 644 triggerIndex = 0; 645 this.matchBytes = Objects.requireNonNull(matchBytes, "matchBytes"); 646 this.replaceBytes = Objects.requireNonNull(replaceBytes, "replaceBytes"); 647 matchIndex = 0; 648 } 649 650 651 public void write(int b) throws IOException { 652 b = b & 0xff; 653 if (matchBytes.length == 0) { 654 // fast path, no match 655 out.write(b); 656 log.write(b); 657 bytesOut++; 658 return; 659 } 660 // if trigger not satisfied, keep looking 661 if (triggerBytes.length != 0 && triggerIndex < triggerBytes.length) { 662 out.write(b); 663 log.write(b); 664 bytesOut++; 665 666 triggerIndex = (b == (triggerBytes[triggerIndex] & 0xff)) 667 ? ++triggerIndex // matching advance 668 : 0; // no match, reset 669 } else { 670 // trigger not used or has been satisfied 671 if (b == (matchBytes[matchIndex] & 0xff)) { 672 if (++matchIndex >= matchBytes.length) { 673 matchIndex = 0; 674 triggerIndex = 0; // match/replace ok, reset trigger 675 DEBUG("TestSocketFactory MatchReplace %s replaced %d bytes " + 676 "at offset: %d (x%04x)%n", 677 name, replaceBytes.length, bytesOut, bytesOut); 678 out.write(replaceBytes); 679 log.write(replaceBytes); 680 bytesOut += replaceBytes.length; 681 } 682 } else { 683 if (matchIndex > 0) { 684 // mismatch, write out any that matched already 685 DEBUG("Partial match %s matched %d bytes at offset: %d (0x%04x), " + 686 " expected: x%02x, actual: x%02x%n", 687 name, matchIndex, bytesOut, bytesOut, matchBytes[matchIndex], b); 688 out.write(matchBytes, 0, matchIndex); 689 log.write(matchBytes, 0, matchIndex); 690 bytesOut += matchIndex; 691 matchIndex = 0; 692 } 693 if (b == (matchBytes[matchIndex] & 0xff)) { 694 matchIndex++; 695 } else { 696 out.write(b); 697 log.write(b); 698 bytesOut++; 699 } 700 } 701 } 702 } 703 704 public void flush() throws IOException { 705 if (matchIndex > 0) { 706 // write out any that matched already to avoid consumer hang. 707 // Match/replace across a flush is not supported. 708 DEBUG( "Flush partial match %s matched %d bytes at offset: %d (0x%04x)%n", 709 name, matchIndex, bytesOut, bytesOut); 710 out.write(matchBytes, 0, matchIndex); 711 log.write(matchBytes, 0, matchIndex); 712 bytesOut += matchIndex; 713 matchIndex = 0; 714 } 715 } 716 717 @Override 718 public String toString() { 719 return String.format("%s: Out: (%d)", name, bytesOut); 720 } 721 } 722 723 private static byte[] obj1Data = new byte[] { 724 0x7e, 0x7e, 0x7e, 725 (byte) 0x80, 0x05, 726 0x7f, 0x7f, 0x7f, 727 0x73, 0x72, 0x00, 0x10, // TC_OBJECT, TC_CLASSDESC, length = 16 728 (byte)'j', (byte)'a', (byte)'v', (byte)'a', (byte)'.', 729 (byte)'l', (byte)'a', (byte)'n', (byte)'g', (byte)'.', 730 (byte)'n', (byte)'u', (byte)'m', (byte)'b', (byte)'e', (byte)'r' 731 }; 732 private static byte[] obj1Result = new byte[] { 733 0x7e, 0x7e, 0x7e, 734 (byte) 0x80, 0x05, 735 0x7f, 0x7f, 0x7f, 736 0x73, 0x72, 0x00, 0x11, // TC_OBJECT, TC_CLASSDESC, length = 17 737 (byte)'j', (byte)'a', (byte)'v', (byte)'a', (byte)'.', 738 (byte)'l', (byte)'a', (byte)'n', (byte)'g', (byte)'.', 739 (byte)'I', (byte)'n', (byte)'t', (byte)'e', (byte)'g', (byte)'e', (byte)'r' 740 }; 741 private static byte[] obj1Trigger = new byte[] { 742 (byte) 0x80, 0x05 743 }; 744 private static byte[] obj1Trigger2 = new byte[] { 745 0x7D, 0x7D, 0x7D, 0x7D, 746 }; 747 private static byte[] obj1Trigger3 = new byte[] { 748 0x7F, 749 }; 750 private static byte[] obj1Match = new byte[] { 751 0x73, 0x72, 0x00, 0x10, // TC_OBJECT, TC_CLASSDESC, length = 16 752 (byte)'j', (byte)'a', (byte)'v', (byte)'a', (byte)'.', 753 (byte)'l', (byte)'a', (byte)'n', (byte)'g', (byte)'.', 754 (byte)'n', (byte)'u', (byte)'m', (byte)'b', (byte)'e', (byte)'r' 755 }; 756 private static byte[] obj1Repl = new byte[] { 757 0x73, 0x72, 0x00, 0x11, // TC_OBJECT, TC_CLASSDESC, length = 17 758 (byte)'j', (byte)'a', (byte)'v', (byte)'a', (byte)'.', 759 (byte)'l', (byte)'a', (byte)'n', (byte)'g', (byte)'.', 760 (byte)'I', (byte)'n', (byte)'t', (byte)'e', (byte)'g', (byte)'e', (byte)'r' 761 }; 762 763 @DataProvider(name = "MatchReplaceData") 764 static Object[][] matchReplaceData() { 765 byte[] empty = new byte[0]; 766 byte[] byte1 = new byte[]{1, 2, 3, 4, 5, 6}; 767 byte[] bytes2 = new byte[]{1, 2, 4, 3, 5, 6}; 768 byte[] bytes3 = new byte[]{6, 5, 4, 3, 2, 1}; 769 byte[] bytes4 = new byte[]{1, 2, 0x10, 0x20, 0x30, 0x40, 5, 6}; 770 byte[] bytes4a = new byte[]{1, 2, 0x10, 0x20, 0x30, 0x40, 5, 7}; // mostly matches bytes4 771 byte[] bytes5 = new byte[]{0x30, 0x40, 5, 6}; 772 byte[] bytes6 = new byte[]{1, 2, 0x10, 0x20, 0x30}; 773 774 return new Object[][]{ 775 {EMPTY_BYTE_ARRAY, new byte[]{}, new byte[]{}, 776 empty, empty}, 777 {EMPTY_BYTE_ARRAY, new byte[]{}, new byte[]{}, 778 byte1, byte1}, 779 {EMPTY_BYTE_ARRAY, new byte[]{3, 4}, new byte[]{4, 3}, 780 byte1, bytes2}, //swap bytes 781 {EMPTY_BYTE_ARRAY, new byte[]{3, 4}, new byte[]{0x10, 0x20, 0x30, 0x40}, 782 byte1, bytes4}, // insert 783 {EMPTY_BYTE_ARRAY, new byte[]{1, 2, 0x10, 0x20}, new byte[]{}, 784 bytes4, bytes5}, // delete head 785 {EMPTY_BYTE_ARRAY, new byte[]{0x40, 5, 6}, new byte[]{}, 786 bytes4, bytes6}, // delete tail 787 {EMPTY_BYTE_ARRAY, new byte[]{0x40, 0x50}, new byte[]{0x60, 0x50}, 788 bytes4, bytes4}, // partial match, replace nothing 789 {EMPTY_BYTE_ARRAY, bytes4a, bytes3, 790 bytes4, bytes4}, // long partial match, not replaced 791 {EMPTY_BYTE_ARRAY, obj1Match, obj1Repl, 792 obj1Match, obj1Repl}, 793 {obj1Trigger, obj1Match, obj1Repl, 794 obj1Data, obj1Result}, 795 {obj1Trigger3, obj1Match, obj1Repl, 796 obj1Data, obj1Result}, // different trigger, replace 797 {obj1Trigger2, obj1Match, obj1Repl, 798 obj1Data, obj1Data}, // no trigger, no replace 799 }; 800 } 801 802 @Test(dataProvider = "MatchReplaceData") 803 public static void test1(byte[] trigger, byte[] match, byte[] replace, 804 byte[] input, byte[] expected) { 805 System.out.printf("trigger: %s, match: %s, replace: %s%n", Arrays.toString(trigger), 806 Arrays.toString(match), Arrays.toString(replace)); 807 try (ByteArrayOutputStream output = new ByteArrayOutputStream(); 808 ByteArrayOutputStream log = new ByteArrayOutputStream(); 809 OutputStream out = new MatchReplaceOutputStream(output, "test3", 810 log, trigger, match, replace)) { 811 out.write(input); 812 byte[] actual = output.toByteArray(); 813 long index = Arrays.mismatch(actual, expected); 814 815 if (index >= 0) { 816 System.out.printf("array mismatch, offset: %d%n", index); 817 System.out.printf("actual: %s%n", Arrays.toString(actual)); 818 System.out.printf("expected: %s%n", Arrays.toString(expected)); 819 } 820 Assert.assertEquals(actual, expected, "match/replace fail"); 821 } catch (IOException ioe) { 822 Assert.fail("unexpected exception", ioe); 823 } 824 } 825} 826