|
| 1 | +/* |
| 2 | + * Copyright 2024 Google LLC |
| 3 | + * |
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | + * you may not use this file except in compliance with the License. |
| 6 | + * You may obtain a copy of the License at |
| 7 | + * |
| 8 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | + * |
| 10 | + * Unless required by applicable law or agreed to in writing, software |
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | + * See the License for the specific language governing permissions and |
| 14 | + * limitations under the License. |
| 15 | + */ |
| 16 | + |
| 17 | +package com.google.cloud.spanner; |
| 18 | + |
| 19 | +/** |
| 20 | + * HighwayHash algorithm. See <a href="https://github.com/google/highwayhash"> |
| 21 | + * HighwayHash on GitHub</a> |
| 22 | + */ |
| 23 | +public final class HighwayHash { |
| 24 | + private final long[] v0 = new long[4]; |
| 25 | + private final long[] v1 = new long[4]; |
| 26 | + private final long[] mul0 = new long[4]; |
| 27 | + private final long[] mul1 = new long[4]; |
| 28 | + private boolean done = false; |
| 29 | + |
| 30 | + /** |
| 31 | + * @param key0 first 8 bytes of the key |
| 32 | + * @param key1 next 8 bytes of the key |
| 33 | + * @param key2 next 8 bytes of the key |
| 34 | + * @param key3 last 8 bytes of the key |
| 35 | + */ |
| 36 | + public HighwayHash(long key0, long key1, long key2, long key3) { |
| 37 | + reset(key0, key1, key2, key3); |
| 38 | + } |
| 39 | + |
| 40 | + /** |
| 41 | + * @param key array of size 4 with the key to initialize the hash with |
| 42 | + */ |
| 43 | + public HighwayHash(long[] key) { |
| 44 | + if (key.length != 4) { |
| 45 | + throw new IllegalArgumentException(String.format("Key length (%s) must be 4", key.length)); |
| 46 | + } |
| 47 | + reset(key[0], key[1], key[2], key[3]); |
| 48 | + } |
| 49 | + |
| 50 | + /** |
| 51 | + * Updates the hash with 32 bytes of data. If you can read 4 long values |
| 52 | + * from your data efficiently, prefer using update() instead for more speed. |
| 53 | + * @param packet data array which has a length of at least pos + 32 |
| 54 | + * @param pos position in the array to read the first of 32 bytes from |
| 55 | + */ |
| 56 | + public void updatePacket(byte[] packet, int pos) { |
| 57 | + if (pos < 0) { |
| 58 | + throw new IllegalArgumentException(String.format("Pos (%s) must be positive", pos)); |
| 59 | + } |
| 60 | + if (pos + 32 > packet.length) { |
| 61 | + throw new IllegalArgumentException("packet must have at least 32 bytes after pos"); |
| 62 | + } |
| 63 | + long a0 = read64(packet, pos + 0); |
| 64 | + long a1 = read64(packet, pos + 8); |
| 65 | + long a2 = read64(packet, pos + 16); |
| 66 | + long a3 = read64(packet, pos + 24); |
| 67 | + update(a0, a1, a2, a3); |
| 68 | + } |
| 69 | + |
| 70 | + /** |
| 71 | + * Updates the hash with 32 bytes of data given as 4 longs. This function is |
| 72 | + * more efficient than updatePacket when you can use it. |
| 73 | + * @param a0 first 8 bytes in little endian 64-bit long |
| 74 | + * @param a1 next 8 bytes in little endian 64-bit long |
| 75 | + * @param a2 next 8 bytes in little endian 64-bit long |
| 76 | + * @param a3 last 8 bytes in little endian 64-bit long |
| 77 | + */ |
| 78 | + public void update(long a0, long a1, long a2, long a3) { |
| 79 | + if (done) { |
| 80 | + throw new IllegalStateException("Can compute a hash only once per instance"); |
| 81 | + } |
| 82 | + v1[0] += mul0[0] + a0; |
| 83 | + v1[1] += mul0[1] + a1; |
| 84 | + v1[2] += mul0[2] + a2; |
| 85 | + v1[3] += mul0[3] + a3; |
| 86 | + for (int i = 0; i < 4; ++i) { |
| 87 | + mul0[i] ^= (v1[i] & 0xffffffffL) * (v0[i] >>> 32); |
| 88 | + v0[i] += mul1[i]; |
| 89 | + mul1[i] ^= (v0[i] & 0xffffffffL) * (v1[i] >>> 32); |
| 90 | + } |
| 91 | + v0[0] += zipperMerge0(v1[1], v1[0]); |
| 92 | + v0[1] += zipperMerge1(v1[1], v1[0]); |
| 93 | + v0[2] += zipperMerge0(v1[3], v1[2]); |
| 94 | + v0[3] += zipperMerge1(v1[3], v1[2]); |
| 95 | + v1[0] += zipperMerge0(v0[1], v0[0]); |
| 96 | + v1[1] += zipperMerge1(v0[1], v0[0]); |
| 97 | + v1[2] += zipperMerge0(v0[3], v0[2]); |
| 98 | + v1[3] += zipperMerge1(v0[3], v0[2]); |
| 99 | + } |
| 100 | + |
| 101 | + |
| 102 | + /** |
| 103 | + * Updates the hash with the last 1 to 31 bytes of the data. You must use |
| 104 | + * updatePacket first per 32 bytes of the data, if and only if 1 to 31 bytes |
| 105 | + * of the data are not processed after that, updateRemainder must be used for |
| 106 | + * those final bytes. |
| 107 | + * @param bytes data array which has a length of at least pos + size_mod32 |
| 108 | + * @param pos position in the array to start reading size_mod32 bytes from |
| 109 | + * @param size_mod32 the amount of bytes to read |
| 110 | + */ |
| 111 | + public void updateRemainder(byte[] bytes, int pos, int size_mod32) { |
| 112 | + if (pos < 0) { |
| 113 | + throw new IllegalArgumentException(String.format("Pos (%s) must be positive", pos)); |
| 114 | + } |
| 115 | + if (size_mod32 < 0 || size_mod32 >= 32) { |
| 116 | + throw new IllegalArgumentException( |
| 117 | + String.format("size_mod32 (%s) must be between 0 and 31", size_mod32)); |
| 118 | + } |
| 119 | + if (pos + size_mod32 > bytes.length) { |
| 120 | + throw new IllegalArgumentException("bytes must have at least size_mod32 bytes after pos"); |
| 121 | + } |
| 122 | + int size_mod4 = size_mod32 & 3; |
| 123 | + int remainder = size_mod32 & ~3; |
| 124 | + byte[] packet = new byte[32]; |
| 125 | + for (int i = 0; i < 4; ++i) { |
| 126 | + v0[i] += ((long)size_mod32 << 32) + size_mod32; |
| 127 | + } |
| 128 | + rotate32By(size_mod32, v1); |
| 129 | + for (int i = 0; i < remainder; i++) { |
| 130 | + packet[i] = bytes[pos + i]; |
| 131 | + } |
| 132 | + if ((size_mod32 & 16) != 0) { |
| 133 | + for (int i = 0; i < 4; i++) { |
| 134 | + packet[28 + i] = bytes[pos + remainder + i + size_mod4 - 4]; |
| 135 | + } |
| 136 | + } else { |
| 137 | + if (size_mod4 != 0) { |
| 138 | + packet[16 + 0] = bytes[pos + remainder + 0]; |
| 139 | + packet[16 + 1] = bytes[pos + remainder + (size_mod4 >>> 1)]; |
| 140 | + packet[16 + 2] = bytes[pos + remainder + (size_mod4 - 1)]; |
| 141 | + } |
| 142 | + } |
| 143 | + updatePacket(packet, 0); |
| 144 | + } |
| 145 | + |
| 146 | + /** |
| 147 | + * Computes the hash value after all bytes were processed. Invalidates the |
| 148 | + * state. |
| 149 | + * |
| 150 | + * NOTE: The 64-bit HighwayHash algorithm is declared stable and no longer subject to change. |
| 151 | + * |
| 152 | + * @return 64-bit hash |
| 153 | + */ |
| 154 | + public long finalize64() { |
| 155 | + permuteAndUpdate(); |
| 156 | + permuteAndUpdate(); |
| 157 | + permuteAndUpdate(); |
| 158 | + permuteAndUpdate(); |
| 159 | + done = true; |
| 160 | + return v0[0] + v1[0] + mul0[0] + mul1[0]; |
| 161 | + } |
| 162 | + |
| 163 | + /** |
| 164 | + * Computes the hash value after all bytes were processed. Invalidates the state. |
| 165 | + * |
| 166 | + * @return array of size 2 containing 128-bit hash |
| 167 | + */ |
| 168 | + public long[] finalize128() { |
| 169 | + permuteAndUpdate(); |
| 170 | + permuteAndUpdate(); |
| 171 | + permuteAndUpdate(); |
| 172 | + permuteAndUpdate(); |
| 173 | + permuteAndUpdate(); |
| 174 | + permuteAndUpdate(); |
| 175 | + done = true; |
| 176 | + long[] hash = new long[2]; |
| 177 | + hash[0] = v0[0] + mul0[0] + v1[2] + mul1[2]; |
| 178 | + hash[1] = v0[1] + mul0[1] + v1[3] + mul1[3]; |
| 179 | + return hash; |
| 180 | + } |
| 181 | + |
| 182 | + /** |
| 183 | + * Computes the hash value after all bytes were processed. Invalidates the state. |
| 184 | + * |
| 185 | + * @return array of size 4 containing 256-bit hash |
| 186 | + */ |
| 187 | + public long[] finalize256() { |
| 188 | + permuteAndUpdate(); |
| 189 | + permuteAndUpdate(); |
| 190 | + permuteAndUpdate(); |
| 191 | + permuteAndUpdate(); |
| 192 | + permuteAndUpdate(); |
| 193 | + permuteAndUpdate(); |
| 194 | + permuteAndUpdate(); |
| 195 | + permuteAndUpdate(); |
| 196 | + permuteAndUpdate(); |
| 197 | + permuteAndUpdate(); |
| 198 | + done = true; |
| 199 | + long[] hash = new long[4]; |
| 200 | + modularReduction(v1[1] + mul1[1], v1[0] + mul1[0], |
| 201 | + v0[1] + mul0[1], v0[0] + mul0[0], |
| 202 | + hash, 0); |
| 203 | + modularReduction(v1[3] + mul1[3], v1[2] + mul1[2], |
| 204 | + v0[3] + mul0[3], v0[2] + mul0[2], |
| 205 | + hash, 2); |
| 206 | + return hash; |
| 207 | + } |
| 208 | + private void reset(long key0, long key1, long key2, long key3) { |
| 209 | + mul0[0] = 0xdbe6d5d5fe4cce2fL; |
| 210 | + mul0[1] = 0xa4093822299f31d0L; |
| 211 | + mul0[2] = 0x13198a2e03707344L; |
| 212 | + mul0[3] = 0x243f6a8885a308d3L; |
| 213 | + mul1[0] = 0x3bd39e10cb0ef593L; |
| 214 | + mul1[1] = 0xc0acf169b5f18a8cL; |
| 215 | + mul1[2] = 0xbe5466cf34e90c6cL; |
| 216 | + mul1[3] = 0x452821e638d01377L; |
| 217 | + v0[0] = mul0[0] ^ key0; |
| 218 | + v0[1] = mul0[1] ^ key1; |
| 219 | + v0[2] = mul0[2] ^ key2; |
| 220 | + v0[3] = mul0[3] ^ key3; |
| 221 | + v1[0] = mul1[0] ^ ((key0 >>> 32) | (key0 << 32)); |
| 222 | + v1[1] = mul1[1] ^ ((key1 >>> 32) | (key1 << 32)); |
| 223 | + v1[2] = mul1[2] ^ ((key2 >>> 32) | (key2 << 32)); |
| 224 | + v1[3] = mul1[3] ^ ((key3 >>> 32) | (key3 << 32)); |
| 225 | + } |
| 226 | + |
| 227 | + private long zipperMerge0(long v1, long v0) { |
| 228 | + return (((v0 & 0xff000000L) | (v1 & 0xff00000000L)) >>> 24) | |
| 229 | + (((v0 & 0xff0000000000L) | (v1 & 0xff000000000000L)) >>> 16) | |
| 230 | + (v0 & 0xff0000L) | ((v0 & 0xff00L) << 32) | |
| 231 | + ((v1 & 0xff00000000000000L) >>> 8) | (v0 << 56); |
| 232 | + } |
| 233 | + |
| 234 | + private long zipperMerge1(long v1, long v0) { |
| 235 | + return (((v1 & 0xff000000L) | (v0 & 0xff00000000L)) >>> 24) | |
| 236 | + (v1 & 0xff0000L) | ((v1 & 0xff0000000000L) >>> 16) | |
| 237 | + ((v1 & 0xff00L) << 24) | ((v0 & 0xff000000000000L) >>> 8) | |
| 238 | + ((v1 & 0xffL) << 48) | (v0 & 0xff00000000000000L); |
| 239 | + } |
| 240 | + |
| 241 | + private long read64(byte[] src, int pos) { |
| 242 | + // Mask with 0xffL so that it is 0..255 as long (byte can only be -128..127) |
| 243 | + return (src[pos + 0] & 0xffL) | ((src[pos + 1] & 0xffL) << 8) | |
| 244 | + ((src[pos + 2] & 0xffL) << 16) | ((src[pos + 3] & 0xffL) << 24) | |
| 245 | + ((src[pos + 4] & 0xffL) << 32) | ((src[pos + 5] & 0xffL) << 40) | |
| 246 | + ((src[pos + 6] & 0xffL) << 48) | ((src[pos + 7] & 0xffL) << 56); |
| 247 | + } |
| 248 | + |
| 249 | + private void rotate32By(long count, long[] lanes) { |
| 250 | + for (int i = 0; i < 4; ++i) { |
| 251 | + long half0 = (lanes[i] & 0xffffffffL); |
| 252 | + long half1 = (lanes[i] >>> 32) & 0xffffffffL; |
| 253 | + lanes[i] = ((half0 << count) & 0xffffffffL) | (half0 >>> (32 - count)); |
| 254 | + lanes[i] |= ((long)(((half1 << count) & 0xffffffffL) | |
| 255 | + (half1 >>> (32 - count)))) << 32; |
| 256 | + } |
| 257 | + } |
| 258 | + |
| 259 | + private void permuteAndUpdate() { |
| 260 | + update((v0[2] >>> 32) | (v0[2] << 32), |
| 261 | + (v0[3] >>> 32) | (v0[3] << 32), |
| 262 | + (v0[0] >>> 32) | (v0[0] << 32), |
| 263 | + (v0[1] >>> 32) | (v0[1] << 32)); |
| 264 | + } |
| 265 | + |
| 266 | + private void modularReduction(long a3_unmasked, long a2, long a1, |
| 267 | + long a0, long[] hash, int pos) { |
| 268 | + long a3 = a3_unmasked & 0x3FFFFFFFFFFFFFFFL; |
| 269 | + hash[pos + 1] = a1 ^ ((a3 << 1) | (a2 >>> 63)) ^ ((a3 << 2) | (a2 >>> 62)); |
| 270 | + hash[pos + 0] = a0 ^ (a2 << 1) ^ (a2 << 2); |
| 271 | + } |
| 272 | + |
| 273 | + ////////////////////////////////////////////////////////////////////////////// |
| 274 | + |
| 275 | + /** |
| 276 | + * NOTE: The 64-bit HighwayHash algorithm is declared stable and no longer subject to change. |
| 277 | + * |
| 278 | + * @param data array with data bytes |
| 279 | + * @param offset position of first byte of data to read from |
| 280 | + * @param length number of bytes from data to read |
| 281 | + * @param key array of size 4 with the key to initialize the hash with |
| 282 | + * @return 64-bit hash for the given data |
| 283 | + */ |
| 284 | + public static long hash64(byte[] data, int offset, int length, long[] key) { |
| 285 | + HighwayHash h = new HighwayHash(key); |
| 286 | + h.processAll(data, offset, length); |
| 287 | + return h.finalize64(); |
| 288 | + } |
| 289 | + |
| 290 | + /** |
| 291 | + * @param data array with data bytes |
| 292 | + * @param offset position of first byte of data to read from |
| 293 | + * @param length number of bytes from data to read |
| 294 | + * @param key array of size 4 with the key to initialize the hash with |
| 295 | + * @return array of size 2 containing 128-bit hash for the given data |
| 296 | + */ |
| 297 | + public static long[] hash128(byte[] data, int offset, int length, long[] key) { |
| 298 | + HighwayHash h = new HighwayHash(key); |
| 299 | + h.processAll(data, offset, length); |
| 300 | + return h.finalize128(); |
| 301 | + } |
| 302 | + |
| 303 | + /** |
| 304 | + * @param data array with data bytes |
| 305 | + * @param offset position of first byte of data to read from |
| 306 | + * @param length number of bytes from data to read |
| 307 | + * @param key array of size 4 with the key to initialize the hash with |
| 308 | + * @return array of size 4 containing 256-bit hash for the given data |
| 309 | + */ |
| 310 | + public static long[] hash256(byte[] data, int offset, int length, long[] key) { |
| 311 | + HighwayHash h = new HighwayHash(key); |
| 312 | + h.processAll(data, offset, length); |
| 313 | + return h.finalize256(); |
| 314 | + } |
| 315 | + |
| 316 | + private void processAll(byte[] data, int offset, int length) { |
| 317 | + int i; |
| 318 | + for (i = 0; i + 32 <= length; i += 32) { |
| 319 | + updatePacket(data, offset + i); |
| 320 | + } |
| 321 | + if ((length & 31) != 0) { |
| 322 | + updateRemainder(data, offset + i, length & 31); |
| 323 | + } |
| 324 | + } |
| 325 | + |
| 326 | + public static String convertToHex(long hash, int kPrefixLength) { |
| 327 | + // Perform bitwise right shift |
| 328 | + long shiftedValue = hash >> (64 - kPrefixLength); |
| 329 | + // Convert to hexadecimal and zero-pad to 6 characters |
| 330 | + return String.format("%06X", shiftedValue); // %06X for zero-padded hex string of width 6 |
| 331 | + } |
| 332 | +} |
0 commit comments