ByteArrayMatcher.java

/**
 * Copyright (C) 2022 Christopher J. Stehno
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.github.cjstehno.testthings.match;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.val;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matcher;

import java.util.Arrays;

import static io.github.cjstehno.testthings.match.PredicateMatcher.matchesPredicate;

/**
 * Hamcrest matchers for working with byte arrays.
 */
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public abstract class ByteArrayMatcher extends BaseMatcher<byte[]> {

    /**
     * Matches a byte array equal to the provided byte array.
     *
     * @param bytes the byte array
     * @return the matcher
     */
    public static Matcher<byte[]> arrayEqualTo(final byte[] bytes) {
        return new ArrayEqualToMatcher(bytes);
    }

    /**
     * Matches a byte array starting with the provided byte array.
     *
     * @param prefix the prefix bytes
     * @return the matcher
     */
    public static Matcher<byte[]> arrayStartsWith(final byte[] prefix) {
        return new ArrayStartsWithMatcher(prefix);
    }

    /**
     * Matches a sub-array of bytes in the provided byte array.
     *
     * @param sub the sub-array
     * @return the matcher
     */
    public static Matcher<byte[]> arrayContains(final byte[] sub){
        return new ArrayContainsBytes(sub);
    }

    /**
     * Matches a byte array with the specified length.
     *
     * @param length the byte array length
     * @return the matcher
     */
    public static Matcher<byte[]> arrayLengthIs(final int length) {
        return matchesPredicate(bs -> bs.length == length, "a byte array with length " + length);
    }

    @RequiredArgsConstructor(access = AccessLevel.PRIVATE)
    private static class ArrayContainsBytes extends ByteArrayMatcher {
        private final byte[] subarray;

        @Override public boolean matches(final Object actual) {
            return arrayContains((byte[])actual, subarray);
        }

        @Override public void describeTo(final Description description) {
            description.appendText("an array of bytes containing the sub-array of bytes");
        }

        private static boolean arrayContains(final byte[] bytes, final byte[] sub) {
            for (int b = 0; b < bytes.length; b++) {
                if (bytes[b] == sub[0]) {
                    val matches = checkSub(bytes, b, sub);
                    if (matches) {
                        return true;
                    }
                }
            }
            return false;
        }

        private static boolean checkSub(final byte[] array, final int starting, final byte[] sub) {
            for (int s = 0; s < sub.length; s++) {
                if (array[starting + s] != sub[s]) {
                    return false;
                }
            }
            return true;
        }
    }

    @RequiredArgsConstructor(access = AccessLevel.PRIVATE)
    private static class ArrayStartsWithMatcher extends ByteArrayMatcher {
        private final byte[] prefix;

        @Override public boolean matches(final Object actual) {
            val actualBytes = (byte[]) actual;

            for (int b = 0; b < prefix.length; b++) {
                if (actualBytes[b] != prefix[b]) {
                    return false;
                }
            }

            return true;
        }

        @Override public void describeTo(Description description) {
            description.appendText("an array of bytes starting with the prefix bytes");
        }
    }

    @RequiredArgsConstructor(access = AccessLevel.PRIVATE)
    private static class ArrayEqualToMatcher extends ByteArrayMatcher {
        private final byte[] array;

        @Override public boolean matches(final Object actual) {
            return Arrays.equals(array, (byte[]) actual);
        }

        @Override public void describeTo(Description description) {
            description.appendText("an array of bytes equal to another array of bytes");
        }
    }
}