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