All Downloads are FREE. Search and download functionalities are using the official Maven repository.

migratedb.v1.core.api.Checksum Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2022-2024 The MigrateDB contributors
 *
 * 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 migratedb.v1.core.api;

import org.checkerframework.checker.nullness.qual.Nullable;

import java.io.*;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.DigestOutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;

import static java.util.Collections.emptyIterator;

/**
 * Digest-based checksum that replaces the old Integer checksum.
 */
public final class Checksum {
    private final byte[] bytes;

    private Checksum(byte[] bytes) {
        this.bytes = Objects.requireNonNull(bytes);
    }

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Checksum)) {
            return false;
        }
        var other = (Checksum) obj;
        return Arrays.equals(bytes, other.bytes);
    }

    @Override
    public int hashCode() {
        return Arrays.hashCode(bytes);
    }

    /**
     * Returns Base64-encoded value of this checksum for storage in the schema history table. The output of this method
     * can be passed to the {@link #parse(String)} method in order to reconstitute the checksum.
     *
     * @return This checksum as a Base64-String.
     */
    @Override
    public String toString() {
        return Base64.getEncoder().encodeToString(bytes);
    }

    /**
     * Parses the string value of a checksum. This method accepts the output of {@link #toString()}.
     *
     * @param string String form of checksum to parse.
     *
     * @return Reconstituted checksum.
     */
    public static Checksum parse(String string) {
        return new Checksum(Base64.getDecoder().decode(string));
    }

    public static Builder builder() {
        return new Builder();
    }

    public static final class Builder {
        private final MessageDigest digest = newMessageDigest();
        /**
         * To make sure the digest changes every time a value is added, even if that value is null or an empty byte
         * array/empty stream.
         */
        private byte counter = 0;

        public Builder addBytes(byte @Nullable [] value) {
            return add(value);
        }

        public Builder addBytes(@Nullable InputStream value) {
            return add(value);
        }

        public Builder addString(@Nullable String value) {
            return add(value == null ? null : value.getBytes(StandardCharsets.UTF_8));
        }

        public Builder addLines(@Nullable Reader value) {
            return add(value == null ? emptyIterator() : utf8LinesOf(value));
        }

        public Builder addNumber(@Nullable BigInteger value) {
            return add(value == null ? null : value.toByteArray());
        }

        public Builder addNumber(@Nullable Long value) {
            return addNumber(value == null ? null : BigInteger.valueOf(value));
        }

        /**
         * Returns the final checksum and resets the state of this builder (as if it were freshly constructed).
         */
        public Checksum build() {
            var checksum = new Checksum(digest.digest());
            counter = 0;
            return checksum;
        }

        private Builder add(Iterator bytes) {
            digest.update(counter++);
            while (bytes.hasNext()) {
                digest.update(bytes.next());
            }
            return this;
        }

        private Builder add(@Nullable InputStream bytes) {
            digest.update(counter++);
            if (bytes != null) {
                try (var out = new DigestOutputStream(discardingOutputStream, digest)) {
                    bytes.transferTo(out);
                } catch (IOException e) {
                    throw new UncheckedIOException(e);
                }
            }
            return this;
        }

        private Builder add(byte @Nullable [] bytes) {
            return add(bytes == null ? emptyIterator() : List.of(bytes).iterator());
        }

        private static Iterator utf8LinesOf(Reader reader) {
            var buffered = reader instanceof BufferedReader ? (BufferedReader) reader : new BufferedReader(reader);
            return buffered.lines()
                           .map(it -> it.getBytes(StandardCharsets.UTF_8))
                           .iterator();
        }

        private static MessageDigest newMessageDigest() {
            try {
                return MessageDigest.getInstance("SHA-256");
            } catch (NoSuchAlgorithmException ignored) {
                // Cannot happen, since every JVM must support the algorithm
                throw new AssertionError();
            }
        }

        private static final OutputStream discardingOutputStream = new OutputStream() {
            @Override
            public void write(byte[] b) {
            }

            @Override
            public void write(byte[] b, int off, int len) {
            }

            @Override
            public void write(int b) {
            }
        };
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy