diff --git a/.classpath b/.classpath
index 9aaded0..a680c63 100644
--- a/.classpath
+++ b/.classpath
@@ -3,6 +3,7 @@
+
diff --git a/res/LICENSE.siphash-zackehh.txt b/res/LICENSE.siphash-zackehh.txt
new file mode 100644
index 0000000..45546bf
--- /dev/null
+++ b/res/LICENSE.siphash-zackehh.txt
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2016 Isaac Whitfield
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/siphash-zackehh/.gitignore b/siphash-zackehh/.gitignore
new file mode 100644
index 0000000..b77c57f
--- /dev/null
+++ b/siphash-zackehh/.gitignore
@@ -0,0 +1,19 @@
+*.class
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.ear
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+
+# IntelliJ
+.idea
+*.iml
+
+# build
+target
\ No newline at end of file
diff --git a/siphash-zackehh/.travis.yml b/siphash-zackehh/.travis.yml
new file mode 100644
index 0000000..c5d06f5
--- /dev/null
+++ b/siphash-zackehh/.travis.yml
@@ -0,0 +1,3 @@
+language: java
+script:
+ - mvn clean test jacoco:report coveralls:report
\ No newline at end of file
diff --git a/siphash-zackehh/LICENSE b/siphash-zackehh/LICENSE
new file mode 100644
index 0000000..45546bf
--- /dev/null
+++ b/siphash-zackehh/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2016 Isaac Whitfield
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/siphash-zackehh/README.md b/siphash-zackehh/README.md
new file mode 100644
index 0000000..afe4658
--- /dev/null
+++ b/siphash-zackehh/README.md
@@ -0,0 +1,77 @@
+# SipHash
+
+[](https://travis-ci.org/zackehh/siphash-java) [](https://coveralls.io/github/zackehh/siphash-java?branch=master)
+
+A Java implementation of the SipHash cryptographic hash family. Supports any variation, although defaults to the widely used SipHash-2-4. Can be used with either full input, or used as a streaming digest.
+
+This library was heavily influenced by [veorq's C implementation](https://github.com/veorq/siphash) and [Forward C&C's reference implementation](http://www.forward.com.au/pfod/SipHashJavaLibrary/) - I just decided it was time a Java implementation of SipHash made it onto Maven :).
+
+## Setup
+
+`siphash` is available on Maven central, via Sonatype OSS:
+
+```
+
+ com.zackehh
+ siphash
+ 1.0.0
+
+```
+
+## Usage
+
+There are two ways of using SipHash (see below). Both return a `SipHashResult` which can be used to retrieve the result in various forms. All constructors can take arguments to specify the compression rounds. For further usage, please visit the [documentation](http://www.javadoc.io/doc/com.zackehh/siphash).
+
+#### Full Input Hash
+
+The first is to simple create a `SipHash` instance and use it to repeatedly hash using the same key.
+
+The internal state is immutable, so you can hash many inputs without having to recreate a new `SipHash` instance (unless you want a new key).
+
+```java
+SipHash hasher = new SipHash("0123456789ABCDEF".getBytes());
+
+SipHashResult result = hasher.hash("my-input".getBytes());
+
+System.out.println(result.get()); // 182795880124085484 <-- this can overflow
+System.out.println(result.getHex()); // "2896be26d3374ec"
+System.out.println(result.getHex(true)); // "02896be26d3374ec"
+System.out.println(result.getHex(SipHashCase.UPPER)); // "2896BE26D3374EC"
+System.out.println(result.getHex(true, SipHashCase.UPPER)); // "02896BE26D3374EC"
+```
+
+#### Streaming Hash
+
+The second is to use the library as a streaming hash, meaning you can apply chunks of bytes to the hash as they become available.
+
+Using this method you must create a new digest every time you want to hash a different input as the internal state is mutable.
+
+```java
+SipHashDigest digest = new SipHashDigest("0123456789ABCDEF".getBytes());
+
+digest.update("chu".getBytes());
+digest.update("nked".getBytes());
+digest.update(" string".getBytes());
+
+SipHashResult result = digest.finish();
+
+System.out.println(result.get()); // 3502906798476177428 <-- this can overflow
+System.out.println(result.getHex()); // "309cd32c8c793014"
+System.out.println(result.getHex(true)); // "309cd32c8c793014"
+System.out.println(result.getHex(SipHashCase.UPPER)); // "309CD32C8C793014"
+System.out.println(result.getHex(true, SipHashCase.UPPER)); // "309CD32C8C793014"
+```
+
+## Contributing
+
+If you wish to contribute (awesome!), please file an issue first! All PRs should pass `mvn clean verify` and maintain 100% test coverage.
+
+## Testing
+
+Tests are run using `mvn`. I aim to maintain 100% coverage where possible (both line and branch).
+
+Tests can be run as follows:
+
+```bash
+$ mvn clean verify
+```
\ No newline at end of file
diff --git a/siphash-zackehh/pom.xml b/siphash-zackehh/pom.xml
new file mode 100644
index 0000000..b78eaf3
--- /dev/null
+++ b/siphash-zackehh/pom.xml
@@ -0,0 +1,171 @@
+
+
+ 4.0.0
+
+ com.zackehh
+ siphash
+ 1.1.0-SNAPSHOT
+ jar
+
+ SipHash
+
+ A SipHash implementation in Java.
+
+ https://github.com/zackehh/siphash-java
+
+
+ 0.7.5.201505241946
+ UTF-8
+
+
+
+
+ iwhitfield
+ Isaac Whitfield
+ iw@zackehh.com
+ Appcelerator, Inc.
+ http://www.appcelerator.com
+
+
+
+
+ scm:git:git@github.com:zackehh/siphash-java.git
+ scm:git:git@github.com:zackehh/siphash-java.git
+ git@github.com:zackehh/siphash-java.git
+
+
+
+
+ MIT License
+ http://www.opensource.org/licenses/mit-license.php
+
+
+
+
+
+ ossrh
+ https://oss.sonatype.org/content/repositories/snapshots
+
+
+ ossrh
+ https://oss.sonatype.org/service/local/staging/deploy/maven2/
+
+
+
+
+
+ org.apache.commons
+ commons-lang3
+ 3.4
+
+
+ org.testng
+ testng
+ 6.8.7
+ test
+
+
+
+
+
+ release
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 2.2.1
+
+
+ attach-sources
+
+ jar-no-fork
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 2.9.1
+
+
+ attach-javadocs
+
+ jar
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ 1.5
+
+
+ sign-artifacts
+ verify
+
+ sign
+
+
+
+
+
+ org.sonatype.plugins
+ nexus-staging-maven-plugin
+ 1.6.3
+ true
+
+ ossrh
+ https://oss.sonatype.org/
+ false
+
+
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.3
+
+ 1.7
+ 1.7
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+ ${jacoco.version}
+
+
+ prepare-agent
+
+ prepare-agent
+
+
+
+ report
+ prepare-package
+
+ report
+
+
+
+
+
+ org.eluder.coveralls
+ coveralls-maven-plugin
+ 4.1.0
+
+
+
+
+
\ No newline at end of file
diff --git a/siphash-zackehh/src/main/java/com/zackehh/siphash/SipHash.java b/siphash-zackehh/src/main/java/com/zackehh/siphash/SipHash.java
new file mode 100644
index 0000000..d6d9da3
--- /dev/null
+++ b/siphash-zackehh/src/main/java/com/zackehh/siphash/SipHash.java
@@ -0,0 +1,106 @@
+package com.zackehh.siphash;
+
+import static com.zackehh.siphash.SipHashConstants.*;
+
+/**
+ * Main entry point for SipHash, providing a basic hash
+ * interface. Assuming you have your full String to hash,
+ * you can simply provide it to ${@link SipHash#hash(byte[])}.
+ *
+ * This class can be initialized and stored in case the
+ * developer wishes to use the same key over and over again.
+ *
+ * This avoids the overhead of having to create the initial
+ * key over and over again.
+ *
+ *
+ * {@code
+ * List inputs = Arrays.asList("input1", "input2", "input3");
+ * SipHash hasher = new SipHash("this key is mine".getBytes());
+ * for (int i = 0; i < inputs.size(); i++) {
+ * hasher.hash(inputs.get(i));
+ * }
+ * }
+ *
+ */
+public class SipHash {
+
+ /**
+ * The values of SipHash-c-d, to determine which of the SipHash
+ * family we're using for this hash.
+ */
+ private final int c, d;
+
+ /**
+ * Initial seeded value of v0.
+ */
+ private final long v0;
+
+ /**
+ * Initial seeded value of v1.
+ */
+ private final long v1;
+
+ /**
+ * Initial seeded value of v2.
+ */
+ private final long v2;
+
+ /**
+ * Initial seeded value of v3.
+ */
+ private final long v3;
+
+ /**
+ * Accepts a 16 byte key input, and uses it to initialize
+ * the state of the hash. This uses the default values of
+ * c/d, meaning that we default to SipHash-2-4.
+ *
+ * @param key a 16 byte key input
+ */
+ public SipHash(byte[] key){
+ this(key, DEFAULT_C, DEFAULT_D);
+ }
+
+ /**
+ * Accepts a 16 byte key input, and uses it to initialize
+ * the state of the hash. This constructor allows for
+ * providing the c/d values, allowing the developer to
+ * select any of the SipHash family to use for hashing.
+ *
+ * @param key a 16 byte key input
+ * @param c the number of compression rounds
+ * @param d the number of finalization rounds
+ */
+ public SipHash(byte[] key, int c, int d){
+ this.c = c;
+ this.d = d;
+
+ SipHashKey hashKey = new SipHashKey(key);
+
+ this.v0 = (INITIAL_V0 ^ hashKey.k0);
+ this.v1 = (INITIAL_V1 ^ hashKey.k1);
+ this.v2 = (INITIAL_V2 ^ hashKey.k0);
+ this.v3 = (INITIAL_V3 ^ hashKey.k1);
+ }
+
+ /**
+ * The basic hash implementation provided in the library.
+ * Assuming you have your full input, you can provide it and
+ * it will be hashed based on the values which were provided
+ * to the constructor of this class.
+ *
+ * @param data the bytes to hash
+ * @return a ${@link SipHashResult} instance
+ */
+ public SipHashResult hash(byte[] data) {
+ SipHashDigest digest = new SipHashDigest(v0, v1, v2, v3, c, d);
+
+ for (byte aData : data) {
+ digest.update(aData);
+ }
+
+ return digest.finish();
+ }
+
+}
\ No newline at end of file
diff --git a/siphash-zackehh/src/main/java/com/zackehh/siphash/SipHashCase.java b/siphash-zackehh/src/main/java/com/zackehh/siphash/SipHashCase.java
new file mode 100644
index 0000000..6601394
--- /dev/null
+++ b/siphash-zackehh/src/main/java/com/zackehh/siphash/SipHashCase.java
@@ -0,0 +1,8 @@
+package com.zackehh.siphash;
+
+/**
+ * A basic enum to determine the case of a String.
+ *
+ * A String can either be UPPER or LOWER case.
+ */
+public enum SipHashCase { UPPER, LOWER }
diff --git a/siphash-zackehh/src/main/java/com/zackehh/siphash/SipHashConstants.java b/siphash-zackehh/src/main/java/com/zackehh/siphash/SipHashConstants.java
new file mode 100644
index 0000000..a79d1e9
--- /dev/null
+++ b/siphash-zackehh/src/main/java/com/zackehh/siphash/SipHashConstants.java
@@ -0,0 +1,63 @@
+package com.zackehh.siphash;
+
+/**
+ * Class containing several constants for use alongside
+ * hashing. Fields such as initial states and defaults,
+ * as they will not change throughout hashing.
+ */
+class SipHashConstants {
+
+ /**
+ * This constructor is private, nobody should be
+ * accessing it!
+ *
+ * @throws IllegalAccessException
+ */
+ private SipHashConstants() throws IllegalAccessException {
+ throw new IllegalAccessException();
+ }
+
+ /**
+ * Initial magic number for v0.
+ */
+ static final long INITIAL_V0 = 0x736f6d6570736575L;
+
+ /**
+ * Initial magic number for v1.
+ */
+ static final long INITIAL_V1 = 0x646f72616e646f6dL;
+
+ /**
+ * Initial magic number for v2.
+ */
+ static final long INITIAL_V2 = 0x6c7967656e657261L;
+
+ /**
+ * Initial magic number for v3.
+ */
+ static final long INITIAL_V3 = 0x7465646279746573L;
+
+ /**
+ * The default number of rounds of compression during per block.
+ * This defaults to 2 as the default implementation is SipHash-2-4.
+ */
+ static final int DEFAULT_C = 2;
+
+ /**
+ * The default number of rounds of compression during finalization.
+ * This defaults to 4 as the default implementation is SipHash-2-4.
+ */
+ static final int DEFAULT_D = 4;
+
+ /**
+ * Whether or not we should pad any hashes by default.
+ */
+ static final boolean DEFAULT_PADDING = false;
+
+ /**
+ * The default String casing for any output Hex Strings. We default
+ * to lower case as it's the least expensive path.
+ */
+ static final SipHashCase DEFAULT_CASE = SipHashCase.LOWER;
+
+}
diff --git a/siphash-zackehh/src/main/java/com/zackehh/siphash/SipHashDigest.java b/siphash-zackehh/src/main/java/com/zackehh/siphash/SipHashDigest.java
new file mode 100644
index 0000000..488a3eb
--- /dev/null
+++ b/siphash-zackehh/src/main/java/com/zackehh/siphash/SipHashDigest.java
@@ -0,0 +1,225 @@
+package com.zackehh.siphash;
+
+import static com.zackehh.siphash.SipHashConstants.DEFAULT_C;
+import static com.zackehh.siphash.SipHashConstants.DEFAULT_D;
+
+/**
+ * A streaming implementation of SipHash, to be used when
+ * you don't have all input available at the same time. Chunks
+ * of bytes can be applied as they're received, and will be hashed
+ * accordingly.
+ *
+ * As with ${@link SipHash}, the compression and finalization rounds
+ * can be customized.
+ */
+public class SipHashDigest {
+
+ /**
+ * The values of SipHash-c-d, to determine which of the SipHash
+ * family we're using for this hash.
+ */
+ private final int c, d;
+
+ /**
+ * Initial seeded value of v0.
+ */
+ private long v0;
+
+ /**
+ * Initial seeded value of v1.
+ */
+ private long v1;
+
+ /**
+ * Initial seeded value of v2.
+ */
+ private long v2;
+
+ /**
+ * Initial seeded value of v3.
+ */
+ private long v3;
+
+ /**
+ * A counter to keep track of the length of the input.
+ */
+ private byte input_len = 0;
+
+ /**
+ * A counter to keep track of the current position inside
+ * of a chunk of bytes. Seeing as bytes are applied in chunks
+ * of 8, this is necessary.
+ */
+ private int m_idx = 0;
+
+ /**
+ * The `m` value from the SipHash algorithm. Every 8 bytes, this
+ * value will be applied to the current state of the hash.
+ */
+ private long m;
+
+ /**
+ * Accepts a 16 byte key input, and uses it to initialize
+ * the state of the hash. This uses the default values of
+ * c/d, meaning that we default to SipHash-2-4.
+ *
+ * @param key a 16 byte key input
+ */
+ public SipHashDigest(byte[] key) {
+ this(key, DEFAULT_C, DEFAULT_D);
+ }
+
+ /**
+ * Accepts a 16 byte key input, and uses it to initialize
+ * the state of the hash. This constructor allows for
+ * providing the c/d values, allowing the developer to
+ * select any of the SipHash family to use for hashing.
+ *
+ * @param key a 16 byte key input
+ * @param c the number of compression rounds
+ * @param d the number of finalization rounds
+ */
+ public SipHashDigest(byte[] key, int c, int d) {
+ this.c = c;
+ this.d = d;
+
+ SipHashKey hashKey = new SipHashKey(key);
+
+ this.v0 = SipHashConstants.INITIAL_V0 ^ hashKey.k0;
+ this.v1 = SipHashConstants.INITIAL_V1 ^ hashKey.k1;
+ this.v2 = SipHashConstants.INITIAL_V2 ^ hashKey.k0;
+ this.v3 = SipHashConstants.INITIAL_V3 ^ hashKey.k1;
+ }
+
+ /**
+ * This constructor is used by the ${@link SipHash} implementation,
+ * and takes an initial (seeded) value of v0/v1/v2/v3. This is used
+ * when the key has been pre-calculated. This constructor also
+ * receives the values of `c` and `d` to use in this hash.
+ *
+ * @param v0 an initial seeded v0
+ * @param v1 an initial seeded v1
+ * @param v2 an initial seeded v2
+ * @param v3 an initial seeded v3
+ * @param c the number of compression rounds
+ * @param d the number of finalization rounds
+ */
+ SipHashDigest(long v0, long v1, long v2, long v3, int c, int d) {
+ this.c = c;
+ this.d = d;
+
+ this.v0 = v0;
+ this.v1 = v1;
+ this.v2 = v2;
+ this.v3 = v3;
+ }
+
+ /**
+ * Updates the current state of the hash with a single byte. This
+ * is the streaming implementation which shifts as required to ensure
+ * we can take an arbitrary number of bytes at any given time. We only
+ * apply the block once the index (`m_idx`) has reached 8. The number
+ * of compression rounds is determined by the `c` value passed in by
+ * the developer.
+ *
+ * This method returns this instance, as a way of allowing the developer
+ * to chain.
+ *
+ * @return a ${@link SipHashDigest} instance
+ */
+ public SipHashDigest update(byte b) {
+ input_len++;
+ m |= (((long) b & 0xff) << (m_idx * 8));
+ m_idx++;
+ if (m_idx >= 8) {
+ v3 ^= m;
+ for (int i = 0; i < c; i++) {
+ round();
+ }
+ v0 ^= m;
+ m_idx = 0;
+ m = 0;
+ }
+ return this;
+ }
+
+ /**
+ * A convenience method to allow passing a chunk of bytes at once, rather
+ * than a byte at a time.
+ *
+ * @return a ${@link SipHashDigest} instance
+ */
+ public SipHashDigest update(byte[] bytes) {
+ for (byte b : bytes) {
+ update(b);
+ }
+ return this;
+ }
+
+ /**
+ * Finalizes the hash by padding 0s until the next multiple of
+ * 8 (as we operate in 8 byte chunks). The last byte added to
+ * the hash is the length of the input, which we keep inside the
+ * `input_len` counter. The number of rounds is based on the value
+ * of `d` as specified by the developer.
+ *
+ * This method returns a ${@link SipHashResult}, as no further updates
+ * should occur (i.e. the lack of chaining here shows we're done).
+ *
+ * @return a ${@link SipHashResult} instance
+ */
+ public SipHashResult finish() {
+ byte msgLenMod256 = input_len;
+
+ while (m_idx < 7) {
+ update((byte) 0);
+ }
+ update(msgLenMod256);
+
+ v2 ^= 0xff;
+ for (int i = 0; i < d; i++) {
+ round();
+ }
+
+ return new SipHashResult(v0 ^ v1 ^ v2 ^ v3);
+ }
+
+ /**
+ * Performs the equivalent of SipRound on the provided state.
+ * This method affects the state of this digest, in that it
+ * mutates the v states directly.
+ */
+ private void round() {
+ v0 += v1;
+ v2 += v3;
+ v1 = rotateLeft(v1, 13);
+ v3 = rotateLeft(v3, 16);
+
+ v1 ^= v0;
+ v3 ^= v2;
+ v0 = rotateLeft(v0, 32);
+
+ v2 += v1;
+ v0 += v3;
+ v1 = rotateLeft(v1, 17);
+ v3 = rotateLeft(v3, 21);
+
+ v1 ^= v2;
+ v3 ^= v0;
+ v2 = rotateLeft(v2, 32);
+ }
+
+ /**
+ * Rotates an input number `val` left by `shift` number of bits. Bits which are
+ * pushed off to the left are rotated back onto the right, making this a left
+ * rotation (a circular shift).
+ *
+ * @param val the value to be shifted
+ * @param shift how far left to shift
+ * @return a long value once shifted
+ */
+ private long rotateLeft(long val, int shift) {
+ return (val << shift) | val >>> (64 - shift);
+ }
+
+}
diff --git a/siphash-zackehh/src/main/java/com/zackehh/siphash/SipHashKey.java b/siphash-zackehh/src/main/java/com/zackehh/siphash/SipHashKey.java
new file mode 100644
index 0000000..f6ce2ff
--- /dev/null
+++ b/siphash-zackehh/src/main/java/com/zackehh/siphash/SipHashKey.java
@@ -0,0 +1,53 @@
+package com.zackehh.siphash;
+
+/**
+ * A container class to store both k0 and k1. These
+ * values are created from a 16 byte key passed into
+ * the constructor. This isn't ideal as it's another
+ * alloc, but it'll do for now.
+ */
+class SipHashKey {
+
+ /**
+ * The value of k0.
+ */
+ final long k0;
+
+ /**
+ * The value of k1.
+ */
+ final long k1;
+
+ /**
+ * Accepts a 16 byte input key and converts the
+ * first and last 8 byte chunks to little-endian.
+ * These values become k0 and k1.
+ *
+ * @param key the 16 byte key input
+ */
+ public SipHashKey(byte[] key) {
+ if (key.length != 16) {
+ throw new IllegalArgumentException("Key must be exactly 16 bytes!");
+ }
+ this.k0 = bytesToLong(key, 0);
+ this.k1 = bytesToLong(key, 8);
+ }
+
+ /**
+ * Converts a chunk of 8 bytes to a number in little-endian
+ * format. Accepts an offset to determine where the chunk
+ * begins in the byte array.
+ *
+ * @param b our byte array
+ * @param offset the index to start at
+ * @return a little-endian long representation
+ */
+ private static long bytesToLong(byte[] b, int offset) {
+ long m = 0;
+ for (int i = 0; i < 8; i++) {
+ m |= ((((long) b[i + offset]) & 0xff) << (8 * i));
+ }
+ return m;
+ }
+
+}
diff --git a/siphash-zackehh/src/main/java/com/zackehh/siphash/SipHashResult.java b/siphash-zackehh/src/main/java/com/zackehh/siphash/SipHashResult.java
new file mode 100644
index 0000000..a108b15
--- /dev/null
+++ b/siphash-zackehh/src/main/java/com/zackehh/siphash/SipHashResult.java
@@ -0,0 +1,121 @@
+package com.zackehh.siphash;
+
+import static com.zackehh.siphash.SipHashConstants.DEFAULT_CASE;
+import static com.zackehh.siphash.SipHashConstants.DEFAULT_PADDING;
+
+/**
+ * A container class of the result of a hash. This class exists
+ * to allow the developer to retrieve the result in any format
+ * they like. Currently available formats are `long` and ${@link java.lang.String}.
+ * When retrieving as a String, the developer can specify the case
+ * they want it in, and whether or not we should pad the left side
+ * to 16 characters with 0s.
+ */
+public class SipHashResult {
+
+ /**
+ * The internal hash result.
+ */
+ private final long result;
+
+ /**
+ * A package-private constructor, as only
+ * SipHash should be creating results.
+ *
+ * @param result the result of a hash
+ */
+ SipHashResult(long result){
+ this.result = result;
+ }
+
+ /**
+ * Simply returns the hash result as a long.
+ *
+ * @return the hash value as a long
+ */
+ public long get(){
+ return result;
+ }
+
+ /**
+ * Returns the result as a Hex String, using
+ * the default padding and casing values.
+ *
+ * @return the hash value as a Hex String
+ */
+ public String getHex(){
+ return getHex(DEFAULT_PADDING, DEFAULT_CASE);
+ }
+
+ /**
+ * Returns the result as a Hex String, using
+ * a custom padding value and default casing value.
+ *
+ * @param padding whether or not to pad the string
+ * @return the hash value as a Hex String
+ */
+ public String getHex(boolean padding){
+ return getHex(padding, DEFAULT_CASE);
+ }
+
+ /**
+ * Returns the result as a Hex String, using
+ * a default padding value and custom casing value.
+ *
+ * @param s_case the case to convert the output to
+ * @return the hash value as a Hex String
+ */
+ public String getHex(SipHashCase s_case){
+ return getHex(DEFAULT_PADDING, s_case);
+ }
+
+ /**
+ * Returns the result as a Hex String, taking in
+ * various arguments to customize the output further,
+ * such as casing and padding.
+ *
+ * @param padding whether or not to pad the string
+ * @param s_case the case to convert the output to
+ * @return a Hex String in the custom format
+ */
+ public String getHex(boolean padding, SipHashCase s_case){
+ String str = Long.toHexString(get());
+ if (padding) {
+ str = leftPad(str, 16, "0");
+ }
+ if (s_case == SipHashCase.UPPER) {
+ str = str.toUpperCase();
+ }
+ return str;
+ }
+
+ /**
+ * Modified for https://github.com/Zukero/ATCS
+ * Replaces the StringUtils.leftPad from apache commons, to remove dependency.
+ *
+ * @param str the string to pad
+ * @param len the total desired length
+ * @param pad the padding string
+ * @return str prefixed with enough repetitions of the pad to have a total length matching len
+ */
+ public String leftPad(String str, int len, String pad) {
+ StringBuilder sb = new StringBuilder(len);
+
+ int padlen = len - str.length();
+ int partialPadLen = padlen % pad.length();
+ int padCount = padlen / pad.length();
+
+ while (padCount >= 0) {
+ sb.append(pad);
+ padCount--;
+ }
+
+ if (partialPadLen > 0) {
+ sb.append(pad.substring(0, partialPadLen));
+ }
+
+ sb.append(str);
+
+ return sb.toString();
+ }
+}
diff --git a/siphash-zackehh/src/test/java/com/zackehh/siphash/SipHashCaseTest.java b/siphash-zackehh/src/test/java/com/zackehh/siphash/SipHashCaseTest.java
new file mode 100644
index 0000000..07e27c7
--- /dev/null
+++ b/siphash-zackehh/src/test/java/com/zackehh/siphash/SipHashCaseTest.java
@@ -0,0 +1,27 @@
+package com.zackehh.siphash;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+public class SipHashCaseTest {
+
+ @Test
+ public void ensureAllValues() throws Exception {
+ SipHashCase[] cases = SipHashCase.values();
+
+ Assert.assertEquals(cases[0], SipHashCase.UPPER);
+ Assert.assertEquals(cases[1], SipHashCase.LOWER);
+ }
+
+ @Test
+ public void ensureValueOf() throws Exception {
+ Assert.assertEquals(SipHashCase.valueOf("UPPER"), SipHashCase.UPPER);
+ Assert.assertEquals(SipHashCase.valueOf("LOWER"), SipHashCase.LOWER);
+ }
+
+ @Test(expectedExceptions = IllegalArgumentException.class)
+ public void invalidCaseValueOf() throws Exception {
+ SipHashCase.valueOf("invalid");
+ }
+
+}
diff --git a/siphash-zackehh/src/test/java/com/zackehh/siphash/SipHashConstantsTest.java b/siphash-zackehh/src/test/java/com/zackehh/siphash/SipHashConstantsTest.java
new file mode 100644
index 0000000..2444460
--- /dev/null
+++ b/siphash-zackehh/src/test/java/com/zackehh/siphash/SipHashConstantsTest.java
@@ -0,0 +1,32 @@
+package com.zackehh.siphash;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Modifier;
+
+public class SipHashConstantsTest {
+
+ @Test
+ public void ensureAllConstants() throws Exception {
+ Assert.assertEquals(SipHashConstants.INITIAL_V0, 0x736f6d6570736575L);
+ Assert.assertEquals(SipHashConstants.INITIAL_V1, 0x646f72616e646f6dL);
+ Assert.assertEquals(SipHashConstants.INITIAL_V2, 0x6c7967656e657261L);
+ Assert.assertEquals(SipHashConstants.INITIAL_V3, 0x7465646279746573L);
+ Assert.assertEquals(SipHashConstants.DEFAULT_C, 2);
+ Assert.assertEquals(SipHashConstants.DEFAULT_D, 4);
+ Assert.assertEquals(SipHashConstants.DEFAULT_CASE, SipHashCase.LOWER);
+ Assert.assertEquals(SipHashConstants.DEFAULT_PADDING, false);
+ }
+
+ @Test(expectedExceptions = InvocationTargetException.class)
+ public void ensureCannotInstance() throws Exception {
+ Constructor ctor = SipHashConstants.class.getDeclaredConstructor();
+ ctor.setAccessible(true);
+ Assert.assertTrue(Modifier.isPrivate(ctor.getModifiers()));
+ ctor.newInstance();
+ }
+
+}
diff --git a/siphash-zackehh/src/test/java/com/zackehh/siphash/SipHashDigestTest.java b/siphash-zackehh/src/test/java/com/zackehh/siphash/SipHashDigestTest.java
new file mode 100644
index 0000000..8cd4360
--- /dev/null
+++ b/siphash-zackehh/src/test/java/com/zackehh/siphash/SipHashDigestTest.java
@@ -0,0 +1,148 @@
+package com.zackehh.siphash;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+public class SipHashDigestTest {
+
+ @Test
+ public void initializeDigestWithKey() throws Exception {
+ SipHashDigest sipHash = new SipHashDigest("0123456789ABCDEF".getBytes());
+
+ long v0 = SipHashTestUtils.getPrivateField(sipHash, "v0", Long.class);
+ long v1 = SipHashTestUtils.getPrivateField(sipHash, "v1", Long.class);
+ long v2 = SipHashTestUtils.getPrivateField(sipHash, "v2", Long.class);
+ long v3 = SipHashTestUtils.getPrivateField(sipHash, "v3", Long.class);
+
+ Assert.assertEquals(v0, 4925064773550298181L);
+ Assert.assertEquals(v1, 2461839666708829781L);
+ Assert.assertEquals(v2, 6579568090023412561L);
+ Assert.assertEquals(v3, 3611922228250500171L);
+
+ int c = SipHashTestUtils.getPrivateField(sipHash, "c", Integer.class);
+ int d = SipHashTestUtils.getPrivateField(sipHash, "d", Integer.class);
+
+ Assert.assertEquals(c, SipHashConstants.DEFAULT_C);
+ Assert.assertEquals(d, SipHashConstants.DEFAULT_D);
+ }
+
+ @Test
+ public void initializeDigestWithKeyAndCD() throws Exception {
+ SipHashDigest sipHash = new SipHashDigest("0123456789ABCDEF".getBytes(), 4, 8);
+
+ long v0 = SipHashTestUtils.getPrivateField(sipHash, "v0", Long.class);
+ long v1 = SipHashTestUtils.getPrivateField(sipHash, "v1", Long.class);
+ long v2 = SipHashTestUtils.getPrivateField(sipHash, "v2", Long.class);
+ long v3 = SipHashTestUtils.getPrivateField(sipHash, "v3", Long.class);
+
+ Assert.assertEquals(v0, 4925064773550298181L);
+ Assert.assertEquals(v1, 2461839666708829781L);
+ Assert.assertEquals(v2, 6579568090023412561L);
+ Assert.assertEquals(v3, 3611922228250500171L);
+
+ int c = SipHashTestUtils.getPrivateField(sipHash, "c", Integer.class);
+ int d = SipHashTestUtils.getPrivateField(sipHash, "d", Integer.class);
+
+ Assert.assertEquals(c, 4);
+ Assert.assertEquals(d, 8);
+ }
+
+ @Test
+ public void initializeDigestWithKeyThenHash() throws Exception {
+ SipHashDigest sipHash = new SipHashDigest("0123456789ABCDEF".getBytes());
+
+ long v0 = SipHashTestUtils.getPrivateField(sipHash, "v0", Long.class);
+ long v1 = SipHashTestUtils.getPrivateField(sipHash, "v1", Long.class);
+ long v2 = SipHashTestUtils.getPrivateField(sipHash, "v2", Long.class);
+ long v3 = SipHashTestUtils.getPrivateField(sipHash, "v3", Long.class);
+
+ Assert.assertEquals(v0, 4925064773550298181L);
+ Assert.assertEquals(v1, 2461839666708829781L);
+ Assert.assertEquals(v2, 6579568090023412561L);
+ Assert.assertEquals(v3, 3611922228250500171L);
+
+ int c = SipHashTestUtils.getPrivateField(sipHash, "c", Integer.class);
+ int d = SipHashTestUtils.getPrivateField(sipHash, "d", Integer.class);
+
+ Assert.assertEquals(c, SipHashConstants.DEFAULT_C);
+ Assert.assertEquals(d, SipHashConstants.DEFAULT_D);
+
+ for(byte b : "zymotechnics".getBytes()){
+ sipHash.update(b);
+ }
+
+ SipHashResult hashResult = sipHash.finish();
+
+ Assert.assertEquals(hashResult.get(), 699588702094987020L);
+ Assert.assertEquals(hashResult.getHex(), "9b57037cd3f8f0c");
+ Assert.assertEquals(hashResult.getHex(true), "09b57037cd3f8f0c");
+ Assert.assertEquals(hashResult.getHex(SipHashCase.UPPER), "9B57037CD3F8F0C");
+ Assert.assertEquals(hashResult.getHex(true, SipHashCase.UPPER), "09B57037CD3F8F0C");
+ }
+
+ @Test
+ public void initializeStateWithKeyAndCDThenHash() throws Exception {
+ SipHashDigest sipHash = new SipHashDigest("0123456789ABCDEF".getBytes(), 4, 8);
+
+ long v0 = SipHashTestUtils.getPrivateField(sipHash, "v0", Long.class);
+ long v1 = SipHashTestUtils.getPrivateField(sipHash, "v1", Long.class);
+ long v2 = SipHashTestUtils.getPrivateField(sipHash, "v2", Long.class);
+ long v3 = SipHashTestUtils.getPrivateField(sipHash, "v3", Long.class);
+
+ Assert.assertEquals(v0, 4925064773550298181L);
+ Assert.assertEquals(v1, 2461839666708829781L);
+ Assert.assertEquals(v2, 6579568090023412561L);
+ Assert.assertEquals(v3, 3611922228250500171L);
+
+ int c = SipHashTestUtils.getPrivateField(sipHash, "c", Integer.class);
+ int d = SipHashTestUtils.getPrivateField(sipHash, "d", Integer.class);
+
+ Assert.assertEquals(c, 4);
+ Assert.assertEquals(d, 8);
+
+ for(byte b : "zymotechnics".getBytes()){
+ sipHash.update(b);
+ }
+
+ SipHashResult hashResult = sipHash.finish();
+
+ Assert.assertEquals(hashResult.get(), -3891084581787974112L); // overflow
+ Assert.assertEquals(hashResult.getHex(), "ca0017304f874620");
+ Assert.assertEquals(hashResult.getHex(true), "ca0017304f874620");
+ Assert.assertEquals(hashResult.getHex(SipHashCase.UPPER), "CA0017304F874620");
+ Assert.assertEquals(hashResult.getHex(true, SipHashCase.UPPER), "CA0017304F874620");
+ }
+
+ @Test
+ public void updateWithAByteArrayChunk() throws Exception {
+ SipHashDigest sipHash = new SipHashDigest("0123456789ABCDEF".getBytes());
+
+ long v0 = SipHashTestUtils.getPrivateField(sipHash, "v0", Long.class);
+ long v1 = SipHashTestUtils.getPrivateField(sipHash, "v1", Long.class);
+ long v2 = SipHashTestUtils.getPrivateField(sipHash, "v2", Long.class);
+ long v3 = SipHashTestUtils.getPrivateField(sipHash, "v3", Long.class);
+
+ Assert.assertEquals(v0, 4925064773550298181L);
+ Assert.assertEquals(v1, 2461839666708829781L);
+ Assert.assertEquals(v2, 6579568090023412561L);
+ Assert.assertEquals(v3, 3611922228250500171L);
+
+ int c = SipHashTestUtils.getPrivateField(sipHash, "c", Integer.class);
+ int d = SipHashTestUtils.getPrivateField(sipHash, "d", Integer.class);
+
+ Assert.assertEquals(c, SipHashConstants.DEFAULT_C);
+ Assert.assertEquals(d, SipHashConstants.DEFAULT_D);
+
+ sipHash.update("zymo".getBytes());
+ sipHash.update("techni".getBytes());
+ sipHash.update("cs".getBytes());
+
+ SipHashResult hashResult = sipHash.finish();
+
+ Assert.assertEquals(hashResult.get(), 699588702094987020L);
+ Assert.assertEquals(hashResult.getHex(), "9b57037cd3f8f0c");
+ Assert.assertEquals(hashResult.getHex(true), "09b57037cd3f8f0c");
+ Assert.assertEquals(hashResult.getHex(SipHashCase.UPPER), "9B57037CD3F8F0C");
+ Assert.assertEquals(hashResult.getHex(true, SipHashCase.UPPER), "09B57037CD3F8F0C");
+ }
+}
diff --git a/siphash-zackehh/src/test/java/com/zackehh/siphash/SipHashKeyTest.java b/siphash-zackehh/src/test/java/com/zackehh/siphash/SipHashKeyTest.java
new file mode 100644
index 0000000..5de03e1
--- /dev/null
+++ b/siphash-zackehh/src/test/java/com/zackehh/siphash/SipHashKeyTest.java
@@ -0,0 +1,27 @@
+package com.zackehh.siphash;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+public class SipHashKeyTest {
+
+ @Test
+ public void initializeWithKey() throws Exception {
+ SipHashKey key = new SipHashKey("0123456789ABCDEF".getBytes());
+
+ long k0 = SipHashTestUtils.getPrivateField(key, "k0", Long.class);
+ long k1 = SipHashTestUtils.getPrivateField(key, "k1", Long.class);
+
+ Assert.assertEquals(k0, 3978425819141910832L);
+ Assert.assertEquals(k1, 5063528411713059128L);
+ }
+
+ @Test(
+ expectedExceptions = IllegalArgumentException.class,
+ expectedExceptionsMessageRegExp = "Key must be exactly 16 bytes!"
+ )
+ public void initializeWithKeyTooLong() throws Exception {
+ new SipHashKey("0123456789ABCDEFG".getBytes()); // 17 bytes
+ }
+
+}
diff --git a/siphash-zackehh/src/test/java/com/zackehh/siphash/SipHashTest.java b/siphash-zackehh/src/test/java/com/zackehh/siphash/SipHashTest.java
new file mode 100644
index 0000000..8bf6ef7
--- /dev/null
+++ b/siphash-zackehh/src/test/java/com/zackehh/siphash/SipHashTest.java
@@ -0,0 +1,107 @@
+package com.zackehh.siphash;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+public class SipHashTest {
+
+ @Test
+ public void initializeStateWithKey() throws Exception {
+ SipHash sipHash = new SipHash("0123456789ABCDEF".getBytes());
+
+ long v0 = SipHashTestUtils.getPrivateField(sipHash, "v0", Long.class);
+ long v1 = SipHashTestUtils.getPrivateField(sipHash, "v1", Long.class);
+ long v2 = SipHashTestUtils.getPrivateField(sipHash, "v2", Long.class);
+ long v3 = SipHashTestUtils.getPrivateField(sipHash, "v3", Long.class);
+
+ Assert.assertEquals(v0, 4925064773550298181L);
+ Assert.assertEquals(v1, 2461839666708829781L);
+ Assert.assertEquals(v2, 6579568090023412561L);
+ Assert.assertEquals(v3, 3611922228250500171L);
+
+ int c = SipHashTestUtils.getPrivateField(sipHash, "c", Integer.class);
+ int d = SipHashTestUtils.getPrivateField(sipHash, "d", Integer.class);
+
+ Assert.assertEquals(c, SipHashConstants.DEFAULT_C);
+ Assert.assertEquals(d, SipHashConstants.DEFAULT_D);
+ }
+
+ @Test
+ public void initializeStateWithKeyAndCD() throws Exception {
+ SipHash sipHash = new SipHash("0123456789ABCDEF".getBytes(), 4, 8);
+
+ long v0 = SipHashTestUtils.getPrivateField(sipHash, "v0", Long.class);
+ long v1 = SipHashTestUtils.getPrivateField(sipHash, "v1", Long.class);
+ long v2 = SipHashTestUtils.getPrivateField(sipHash, "v2", Long.class);
+ long v3 = SipHashTestUtils.getPrivateField(sipHash, "v3", Long.class);
+
+ Assert.assertEquals(v0, 4925064773550298181L);
+ Assert.assertEquals(v1, 2461839666708829781L);
+ Assert.assertEquals(v2, 6579568090023412561L);
+ Assert.assertEquals(v3, 3611922228250500171L);
+
+ int c = SipHashTestUtils.getPrivateField(sipHash, "c", Integer.class);
+ int d = SipHashTestUtils.getPrivateField(sipHash, "d", Integer.class);
+
+ Assert.assertEquals(c, 4);
+ Assert.assertEquals(d, 8);
+ }
+
+ @Test
+ public void initializeStateWithKeyThenHash() throws Exception {
+ SipHash sipHash = new SipHash("0123456789ABCDEF".getBytes());
+
+ long v0 = SipHashTestUtils.getPrivateField(sipHash, "v0", Long.class);
+ long v1 = SipHashTestUtils.getPrivateField(sipHash, "v1", Long.class);
+ long v2 = SipHashTestUtils.getPrivateField(sipHash, "v2", Long.class);
+ long v3 = SipHashTestUtils.getPrivateField(sipHash, "v3", Long.class);
+
+ Assert.assertEquals(v0, 4925064773550298181L);
+ Assert.assertEquals(v1, 2461839666708829781L);
+ Assert.assertEquals(v2, 6579568090023412561L);
+ Assert.assertEquals(v3, 3611922228250500171L);
+
+ int c = SipHashTestUtils.getPrivateField(sipHash, "c", Integer.class);
+ int d = SipHashTestUtils.getPrivateField(sipHash, "d", Integer.class);
+
+ Assert.assertEquals(c, SipHashConstants.DEFAULT_C);
+ Assert.assertEquals(d, SipHashConstants.DEFAULT_D);
+
+ SipHashResult hashResult = sipHash.hash("zymotechnics".getBytes());
+
+ Assert.assertEquals(hashResult.get(), 699588702094987020L);
+ Assert.assertEquals(hashResult.getHex(), "9b57037cd3f8f0c");
+ Assert.assertEquals(hashResult.getHex(true), "09b57037cd3f8f0c");
+ Assert.assertEquals(hashResult.getHex(SipHashCase.UPPER), "9B57037CD3F8F0C");
+ Assert.assertEquals(hashResult.getHex(true, SipHashCase.UPPER), "09B57037CD3F8F0C");
+ }
+
+ @Test
+ public void initializeStateWithKeyAndCDThenHash() throws Exception {
+ SipHash sipHash = new SipHash("0123456789ABCDEF".getBytes(), 4, 8);
+
+ long v0 = SipHashTestUtils.getPrivateField(sipHash, "v0", Long.class);
+ long v1 = SipHashTestUtils.getPrivateField(sipHash, "v1", Long.class);
+ long v2 = SipHashTestUtils.getPrivateField(sipHash, "v2", Long.class);
+ long v3 = SipHashTestUtils.getPrivateField(sipHash, "v3", Long.class);
+
+ Assert.assertEquals(v0, 4925064773550298181L);
+ Assert.assertEquals(v1, 2461839666708829781L);
+ Assert.assertEquals(v2, 6579568090023412561L);
+ Assert.assertEquals(v3, 3611922228250500171L);
+
+ int c = SipHashTestUtils.getPrivateField(sipHash, "c", Integer.class);
+ int d = SipHashTestUtils.getPrivateField(sipHash, "d", Integer.class);
+
+ Assert.assertEquals(c, 4);
+ Assert.assertEquals(d, 8);
+
+ SipHashResult hashResult = sipHash.hash("zymotechnics".getBytes());
+
+ Assert.assertEquals(hashResult.get(), -3891084581787974112L); // overflow
+ Assert.assertEquals(hashResult.getHex(), "ca0017304f874620");
+ Assert.assertEquals(hashResult.getHex(true), "ca0017304f874620");
+ Assert.assertEquals(hashResult.getHex(SipHashCase.UPPER), "CA0017304F874620");
+ Assert.assertEquals(hashResult.getHex(true, SipHashCase.UPPER), "CA0017304F874620");
+ }
+}
diff --git a/siphash-zackehh/src/test/java/com/zackehh/siphash/SipHashTestUtils.java b/siphash-zackehh/src/test/java/com/zackehh/siphash/SipHashTestUtils.java
new file mode 100644
index 0000000..14a8fa6
--- /dev/null
+++ b/siphash-zackehh/src/test/java/com/zackehh/siphash/SipHashTestUtils.java
@@ -0,0 +1,13 @@
+package com.zackehh.siphash;
+
+import java.lang.reflect.Field;
+
+public class SipHashTestUtils {
+
+ static T getPrivateField(Object obj, String name, Class clazz) throws Exception {
+ Field f = obj.getClass().getDeclaredField(name);
+ f.setAccessible(true);
+ return clazz.cast(f.get(obj));
+ }
+
+}
diff --git a/src/com/gpl/rpg/atcontentstudio/ATContentStudio.java b/src/com/gpl/rpg/atcontentstudio/ATContentStudio.java
index 1752767..d5a3cd4 100644
--- a/src/com/gpl/rpg/atcontentstudio/ATContentStudio.java
+++ b/src/com/gpl/rpg/atcontentstudio/ATContentStudio.java
@@ -19,6 +19,8 @@ import com.gpl.rpg.atcontentstudio.model.Workspace;
import com.gpl.rpg.atcontentstudio.ui.StudioFrame;
import com.gpl.rpg.atcontentstudio.ui.WorkerDialog;
import com.gpl.rpg.atcontentstudio.ui.WorkspaceSelector;
+import com.gpl.rpg.atcontentstudio.utils.HashUtils;
+import com.zackehh.siphash.SipHash;
public class ATContentStudio {
diff --git a/src/com/gpl/rpg/atcontentstudio/model/WorkspaceSettings.java b/src/com/gpl/rpg/atcontentstudio/model/WorkspaceSettings.java
index 090be12..02d0686 100644
--- a/src/com/gpl/rpg/atcontentstudio/model/WorkspaceSettings.java
+++ b/src/com/gpl/rpg/atcontentstudio/model/WorkspaceSettings.java
@@ -11,6 +11,8 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import javax.swing.ComboBoxModel;
+
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
@@ -39,8 +41,11 @@ public class WorkspaceSettings {
public static Boolean DEFAULT_USE_SYS_IMG_EDITOR = false;
public Setting useSystemDefaultImageEditor = new PrimitiveSetting("useSystemDefaultImageEditor", DEFAULT_USE_SYS_IMG_EDITOR);
public static String DEFAULT_IMG_EDITOR_COMMAND = "gimp";
+ public static String[] LANGUAGE_LIST = new String[]{null, "de", "ru", "pl", "fr", "it", "es", "nl", "uk", "ca", "sv", "pt", "pt_BR", "zh_Hant", "zh_Hans", "ja", "cs", "tr", "ko", "hu", "sl", "bg", "id", "fi", "th", "gl", "ms" ,"pa", "az"};
public Setting imageEditorCommand = new PrimitiveSetting("imageEditorCommand", DEFAULT_IMG_EDITOR_COMMAND);
+ public Setting translatorLanguage = new NullDefaultPrimitiveSetting("translatorLanguage");
+
public List> settings = new ArrayList>();
public WorkspaceSettings(Workspace parent) {
@@ -50,6 +55,7 @@ public class WorkspaceSettings {
settings.add(useSystemDefaultImageViewer);
settings.add(useSystemDefaultImageEditor);
settings.add(imageEditorCommand);
+ settings.add(translatorLanguage);
file = new File(parent.baseFolder, FILENAME);
if (file.exists()) {
load(file);
@@ -174,6 +180,18 @@ public class WorkspaceSettings {
}
+ public class NullDefaultPrimitiveSetting extends PrimitiveSetting {
+
+ public NullDefaultPrimitiveSetting(String id) {
+ super(id, null);
+ }
+
+ @Override
+ public void saveToJson(Map json) {
+ if (value != null) json.put(id, value);
+ }
+ }
+
public class ListSetting extends Setting> {
public ListSetting(String id, List defaultValue) {
diff --git a/src/com/gpl/rpg/atcontentstudio/ui/AboutEditor.java b/src/com/gpl/rpg/atcontentstudio/ui/AboutEditor.java
index 6d11b91..314f99f 100644
--- a/src/com/gpl/rpg/atcontentstudio/ui/AboutEditor.java
+++ b/src/com/gpl/rpg/atcontentstudio/ui/AboutEditor.java
@@ -72,6 +72,9 @@ public class AboutEditor extends Editor {
"BeanShell by Pat Niemeyer. " +
"License: LGPL v3 " +
" " +
+ "A slightly modified version of SipHash for Java by Isaac Whitfield. " +
+ "License: MIT License " +
+ " " +
"See the tabs below to find the full license text for each of these. " +
" " +
"The Windows installer was created with: " +
@@ -121,6 +124,7 @@ public class AboutEditor extends Editor {
editorTabsHolder.add("libtiled-java License", getInfoPane(new Scanner(ATContentStudio.class.getResourceAsStream("/LICENSE.libtiled.txt"), "UTF-8").useDelimiter("\\A").next(), "text/text"));
editorTabsHolder.add("prefuse License", getInfoPane(new Scanner(ATContentStudio.class.getResourceAsStream("/license-prefuse.txt"), "UTF-8").useDelimiter("\\A").next(), "text/text"));
editorTabsHolder.add("BeanShell License", getInfoPane(new Scanner(ATContentStudio.class.getResourceAsStream("/LICENSE.LGPLv3.txt"), "UTF-8").useDelimiter("\\A").next(), "text/text"));
+ editorTabsHolder.add("SipHash for Java License", getInfoPane(new Scanner(ATContentStudio.class.getResourceAsStream("/LICENSE.siphash-zackehh.txt"), "UTF-8").useDelimiter("\\A").next(), "text/text"));
editorTabsHolder.add("ATCS License", getInfoPane(new Scanner(ATContentStudio.class.getResourceAsStream("/LICENSE.GPLv3.txt"), "UTF-8").useDelimiter("\\A").next(), "text/text"));
}
diff --git a/src/com/gpl/rpg/atcontentstudio/ui/Editor.java b/src/com/gpl/rpg/atcontentstudio/ui/Editor.java
index 9811eb8..9b9c08d 100644
--- a/src/com/gpl/rpg/atcontentstudio/ui/Editor.java
+++ b/src/com/gpl/rpg/atcontentstudio/ui/Editor.java
@@ -2,6 +2,8 @@ package com.gpl.rpg.atcontentstudio.ui;
import java.awt.BorderLayout;
import java.awt.Component;
+import java.awt.Cursor;
+import java.awt.Desktop;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
@@ -10,6 +12,9 @@ import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -48,6 +53,7 @@ import com.gpl.rpg.atcontentstudio.Notification;
import com.gpl.rpg.atcontentstudio.model.GameDataElement;
import com.gpl.rpg.atcontentstudio.model.Project;
import com.gpl.rpg.atcontentstudio.model.ProjectElementListener;
+import com.gpl.rpg.atcontentstudio.model.Workspace;
import com.gpl.rpg.atcontentstudio.model.gamedata.ActorCondition;
import com.gpl.rpg.atcontentstudio.model.gamedata.Dialogue;
import com.gpl.rpg.atcontentstudio.model.gamedata.Droplist;
@@ -57,6 +63,7 @@ import com.gpl.rpg.atcontentstudio.model.gamedata.JSONElement;
import com.gpl.rpg.atcontentstudio.model.gamedata.NPC;
import com.gpl.rpg.atcontentstudio.model.gamedata.Quest;
import com.gpl.rpg.atcontentstudio.model.maps.TMXMap;
+import com.gpl.rpg.atcontentstudio.utils.HashUtils;
import com.jidesoft.swing.ComboBoxSearchable;
import com.jidesoft.swing.JideBoxLayout;
@@ -108,6 +115,58 @@ public abstract class Editor extends JPanel implements ProjectElementListener {
return addTextField(pane, label, value, false, nullListener);
}
+ public static JTextField addTranslatableTextField(JPanel pane, String label, String initialValue, boolean editable, final FieldUpdateListener listener) {
+ final JTextField tfField = addTextField(pane, label, initialValue, editable, listener);
+ if (Workspace.activeWorkspace.settings.translatorLanguage.getCurrentValue() != null) {
+ final JLabel translateLinkLabel = new JLabel(getWeblateLabelLink(initialValue));
+ pane.add(translateLinkLabel, JideBoxLayout.FIX);
+ tfField.getDocument().addDocumentListener(new DocumentListener() {
+ @Override
+ public void removeUpdate(DocumentEvent e) {
+ translateLinkLabel.setText(getWeblateLabelLink(tfField.getText()));
+ translateLinkLabel.revalidate();
+ translateLinkLabel.repaint();
+ }
+ @Override
+ public void insertUpdate(DocumentEvent e) {
+ translateLinkLabel.setText(getWeblateLabelLink(tfField.getText()));
+ translateLinkLabel.revalidate();
+ translateLinkLabel.repaint();
+ }
+ @Override
+ public void changedUpdate(DocumentEvent e) {
+ translateLinkLabel.setText(getWeblateLabelLink(tfField.getText()));
+ translateLinkLabel.revalidate();
+ translateLinkLabel.repaint();
+ }
+ });
+ translateLinkLabel.setCursor(new Cursor(Cursor.HAND_CURSOR));
+ translateLinkLabel.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseClicked(MouseEvent e) {
+ if (e.getButton() == MouseEvent.BUTTON1) {
+ try {
+ Desktop.getDesktop().browse(new URI(getWeblateLabelURI(tfField.getText())));
+ } catch (IOException e1) {
+ e1.printStackTrace();
+ } catch (URISyntaxException e1) {
+ e1.printStackTrace();
+ }
+ }
+ }
+ });
+ }
+ return tfField;
+ }
+
+ public static String getWeblateLabelLink(String text) {
+ return "Translate on weblate";
+ }
+
+ public static String getWeblateLabelURI(String text) {
+ return "https://hosted.weblate.org/translate/andors-trail/game-content/"+Workspace.activeWorkspace.settings.translatorLanguage.getCurrentValue()+"/?checksum="+HashUtils.weblateHash(text, "");
+ }
+
public static JTextField addTextField(JPanel pane, String label, String initialValue, boolean editable, final FieldUpdateListener listener) {
JPanel tfPane = new JPanel();
tfPane.setLayout(new JideBoxLayout(tfPane, JideBoxLayout.LINE_AXIS, 6));
@@ -151,6 +210,51 @@ public abstract class Editor extends JPanel implements ProjectElementListener {
return tfField;
}
+
+ public static JTextArea addTranslatableTextArea(JPanel pane, String label, String initialValue, boolean editable, final FieldUpdateListener listener) {
+ final JTextArea tfArea = addTextArea(pane, label, initialValue, editable, listener);
+ if (Workspace.activeWorkspace.settings.translatorLanguage.getCurrentValue() != null) {
+ final JLabel translateLinkLabel = new JLabel(getWeblateLabelLink(initialValue));
+ pane.add(translateLinkLabel, JideBoxLayout.FIX);
+ tfArea.getDocument().addDocumentListener(new DocumentListener() {
+ @Override
+ public void removeUpdate(DocumentEvent e) {
+ translateLinkLabel.setText(getWeblateLabelLink(tfArea.getText().replaceAll("\n", Matcher.quoteReplacement("\n"))));
+ translateLinkLabel.revalidate();
+ translateLinkLabel.repaint();
+ }
+ @Override
+ public void insertUpdate(DocumentEvent e) {
+ translateLinkLabel.setText(getWeblateLabelLink(tfArea.getText().replaceAll("\n", Matcher.quoteReplacement("\n"))));
+ translateLinkLabel.revalidate();
+ translateLinkLabel.repaint();
+ }
+ @Override
+ public void changedUpdate(DocumentEvent e) {
+ translateLinkLabel.setText(getWeblateLabelLink(tfArea.getText().replaceAll("\n", Matcher.quoteReplacement("\n"))));
+ translateLinkLabel.revalidate();
+ translateLinkLabel.repaint();
+ }
+ });
+ translateLinkLabel.setCursor(new Cursor(Cursor.HAND_CURSOR));
+ translateLinkLabel.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseClicked(MouseEvent e) {
+ if (e.getButton() == MouseEvent.BUTTON1) {
+ try {
+ Desktop.getDesktop().browse(new URI(getWeblateLabelURI(tfArea.getText().replaceAll("\n", Matcher.quoteReplacement("\n")))));
+ } catch (IOException e1) {
+ e1.printStackTrace();
+ } catch (URISyntaxException e1) {
+ e1.printStackTrace();
+ }
+ }
+ }
+ });
+ }
+ return tfArea;
+ }
+
public static JTextArea addTextArea(JPanel pane, String label, String initialValue, boolean editable, final FieldUpdateListener listener) {
String text= initialValue == null ? "" : initialValue.replaceAll("\\n", "\n");
diff --git a/src/com/gpl/rpg/atcontentstudio/ui/WorkspaceSettingsEditor.java b/src/com/gpl/rpg/atcontentstudio/ui/WorkspaceSettingsEditor.java
index 48e57da..505634b 100644
--- a/src/com/gpl/rpg/atcontentstudio/ui/WorkspaceSettingsEditor.java
+++ b/src/com/gpl/rpg/atcontentstudio/ui/WorkspaceSettingsEditor.java
@@ -6,7 +6,10 @@ import java.awt.event.ActionListener;
import javax.swing.ButtonGroup;
import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
import javax.swing.JDialog;
+import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JScrollPane;
@@ -28,6 +31,9 @@ public class WorkspaceSettingsEditor extends JDialog {
JRadioButton useSystemDefaultImageViewerButton, useSystemDefaultImageEditorButton, useCustomImageEditorButton;
JTextField imageEditorCommandField;
+ JCheckBox translatorModeBox;
+ JComboBox translatorLanguagesBox;
+
public WorkspaceSettingsEditor(WorkspaceSettings settings) {
@@ -46,6 +52,7 @@ public class WorkspaceSettingsEditor extends JDialog {
pane.add(getExternalToolsPane(), JideBoxLayout.FIX);
+ pane.add(getTranslatorModePane(), JideBoxLayout.FIX);
pane.add(new JPanel(), JideBoxLayout.VARY);
buttonPane.add(new JPanel(), JideBoxLayout.VARY);
@@ -147,6 +154,32 @@ public class WorkspaceSettingsEditor extends JDialog {
return pane;
}
+ public JPanel getTranslatorModePane() {
+ CollapsiblePanel pane = new CollapsiblePanel("Translator options");
+ pane.setLayout(new JideBoxLayout(pane, JideBoxLayout.PAGE_AXIS));
+
+ translatorModeBox = new JCheckBox("Activate translator mode");
+ pane.add(translatorModeBox, JideBoxLayout.FIX);
+
+ JPanel langPane = new JPanel();
+ langPane.setLayout(new JideBoxLayout(langPane, JideBoxLayout.LINE_AXIS));
+ langPane.add(new JLabel("Language code: "), JideBoxLayout.FIX);
+ translatorLanguagesBox = new JComboBox(WorkspaceSettings.LANGUAGE_LIST);
+ langPane.add(translatorLanguagesBox);
+ pane.add(langPane, JideBoxLayout.FIX);
+
+ translatorModeBox.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ translatorLanguagesBox.setEnabled(translatorModeBox.isSelected());
+ }
+ });
+
+ pane.add(new JLabel("If your language isn't here, complain on the forums at https://andorstrail.com/"), JideBoxLayout.FIX);
+
+ return pane;
+ }
+
public void loadFromModel() {
//Tiled
useSystemDefaultMapEditorButton.setSelected(settings.useSystemDefaultMapEditor.getCurrentValue());
@@ -157,6 +190,14 @@ public class WorkspaceSettingsEditor extends JDialog {
useSystemDefaultImageEditorButton.setSelected(settings.useSystemDefaultImageEditor.getCurrentValue());
useCustomImageEditorButton.setSelected(!(settings.useSystemDefaultImageViewer.getCurrentValue() || settings.useSystemDefaultImageEditor.getCurrentValue()));
imageEditorCommandField.setText(settings.imageEditorCommand.getCurrentValue());
+ //Translator
+ if (settings.translatorLanguage.getCurrentValue() != null) {
+ translatorModeBox.setSelected(true);
+ translatorLanguagesBox.setSelectedItem(settings.translatorLanguage.getCurrentValue());
+ } else {
+ translatorModeBox.setSelected(false);
+ translatorLanguagesBox.setSelectedItem(null);
+ }
}
public void pushToModel() {
@@ -167,7 +208,12 @@ public class WorkspaceSettingsEditor extends JDialog {
settings.useSystemDefaultImageViewer.setCurrentValue(useSystemDefaultImageViewerButton.isSelected());
settings.useSystemDefaultImageEditor.setCurrentValue(useSystemDefaultImageEditorButton.isSelected());
settings.imageEditorCommand.setCurrentValue(imageEditorCommandField.getText());
-
+ //Translator
+ if (translatorModeBox.isSelected()) {
+ settings.translatorLanguage.setCurrentValue((String)translatorLanguagesBox.getSelectedItem());
+ } else {
+ settings.translatorLanguage.resetDefault();
+ }
settings.save();
}
diff --git a/src/com/gpl/rpg/atcontentstudio/ui/gamedataeditors/ActorConditionEditor.java b/src/com/gpl/rpg/atcontentstudio/ui/gamedataeditors/ActorConditionEditor.java
index 42c5e42..fda7f13 100644
--- a/src/com/gpl/rpg/atcontentstudio/ui/gamedataeditors/ActorConditionEditor.java
+++ b/src/com/gpl/rpg/atcontentstudio/ui/gamedataeditors/ActorConditionEditor.java
@@ -76,7 +76,7 @@ public class ActorConditionEditor extends JSONElementEditor {
acIcon = createButtonPane(pane, ac.getProject(), ac, ActorCondition.class, ac.getImage(), Spritesheet.Category.actorcondition, listener);
idField = addTextField(pane, "Internal ID: ", ac.id, ac.writable, listener);
- nameField = addTextField(pane, "Display name: ", ac.display_name, ac.writable, listener);
+ nameField = addTranslatableTextField(pane, "Display name: ", ac.display_name, ac.writable, listener);
categoryBox = addEnumValueBox(pane, "Category: ", ActorCondition.ACCategory.values(), ac.category, ac.writable, listener);
positiveBox = addIntegerBasedCheckBox(pane, "Positive", ac.positive, ac.writable, listener);
stackingBox = addIntegerBasedCheckBox(pane, "Stacking", ac.stacking, ac.writable, listener);
diff --git a/src/com/gpl/rpg/atcontentstudio/ui/gamedataeditors/DialogueEditor.java b/src/com/gpl/rpg/atcontentstudio/ui/gamedataeditors/DialogueEditor.java
index b284535..7a518f7 100644
--- a/src/com/gpl/rpg/atcontentstudio/ui/gamedataeditors/DialogueEditor.java
+++ b/src/com/gpl/rpg/atcontentstudio/ui/gamedataeditors/DialogueEditor.java
@@ -165,7 +165,7 @@ public class DialogueEditor extends JSONElementEditor {
createButtonPane(pane, dialogue.getProject(), dialogue, Dialogue.class, dialogue.getImage(), null, listener);
idField = addTextField(pane, "Internal ID: ", dialogue.id, dialogue.writable, listener);
- messageField = addTextArea(pane, "Message: ", dialogue.message, dialogue.writable, listener);
+ messageField = addTranslatableTextArea(pane, "Message: ", dialogue.message, dialogue.writable, listener);
switchToNpcBox = addNPCBox(pane, dialogue.getProject(), "Switch active NPC to: ", dialogue.switch_to_npc, dialogue.writable, listener);
CollapsiblePanel rewards = new CollapsiblePanel("Reaching this phrase gives the following rewards: ");
diff --git a/src/com/gpl/rpg/atcontentstudio/ui/gamedataeditors/ItemCategoryEditor.java b/src/com/gpl/rpg/atcontentstudio/ui/gamedataeditors/ItemCategoryEditor.java
index a843b22..0c2501e 100644
--- a/src/com/gpl/rpg/atcontentstudio/ui/gamedataeditors/ItemCategoryEditor.java
+++ b/src/com/gpl/rpg/atcontentstudio/ui/gamedataeditors/ItemCategoryEditor.java
@@ -52,7 +52,7 @@ public class ItemCategoryEditor extends JSONElementEditor {
idField = addTextField(pane, "Internal ID: ", ic.id, ic.writable, listener);
- nameField = addTextField(pane, "Display name: ", ic.name, ic.writable, listener);
+ nameField = addTranslatableTextField(pane, "Display name: ", ic.name, ic.writable, listener);
typeBox = addEnumValueBox(pane, "Action type: ", ItemCategory.ActionType.values(), ic.action_type, ic.writable, listener);
slotBox = addEnumValueBox(pane, "Inventory slot: ", ItemCategory.InventorySlot.values(), ic.slot, ic.writable, listener);
sizeBox = addEnumValueBox(pane, "Item size: ", ItemCategory.Size.values(), ic.size, ic.writable, listener);
diff --git a/src/com/gpl/rpg/atcontentstudio/ui/gamedataeditors/ItemEditor.java b/src/com/gpl/rpg/atcontentstudio/ui/gamedataeditors/ItemEditor.java
index 0b1674d..2b993c2 100644
--- a/src/com/gpl/rpg/atcontentstudio/ui/gamedataeditors/ItemEditor.java
+++ b/src/com/gpl/rpg/atcontentstudio/ui/gamedataeditors/ItemEditor.java
@@ -149,8 +149,8 @@ public class ItemEditor extends JSONElementEditor {
itemIcon = createButtonPane(pane, item.getProject(), item, Item.class, item.getImage(), Spritesheet.Category.item, listener);
idField = addTextField(pane, "Internal ID: ", item.id, item.writable, listener);
- nameField = addTextField(pane, "Display name: ", item.name, item.writable, listener);
- descriptionField = addTextField(pane, "Description: ", item.description, item.writable, listener);
+ nameField = addTranslatableTextField(pane, "Display name: ", item.name, item.writable, listener);
+ descriptionField = addTranslatableTextField(pane, "Description: ", item.description, item.writable, listener);
typeBox = addEnumValueBox(pane, "Type: ", Item.DisplayType.values(), item.display_type, item.writable, listener);
manualPriceBox = addIntegerBasedCheckBox(pane, "Has manual price", item.has_manual_price, item.writable, listener);
baseManualPrice = item.base_market_cost;
diff --git a/src/com/gpl/rpg/atcontentstudio/ui/gamedataeditors/NPCEditor.java b/src/com/gpl/rpg/atcontentstudio/ui/gamedataeditors/NPCEditor.java
index 8f4ca06..dc7c3f1 100644
--- a/src/com/gpl/rpg/atcontentstudio/ui/gamedataeditors/NPCEditor.java
+++ b/src/com/gpl/rpg/atcontentstudio/ui/gamedataeditors/NPCEditor.java
@@ -171,7 +171,7 @@ public class NPCEditor extends JSONElementEditor {
npcIcon = createButtonPane(pane, npc.getProject(), npc, NPC.class, npc.getImage(), Spritesheet.Category.monster, listener);
idField = addTextField(pane, "Internal ID: ", npc.id, npc.writable, listener);
- nameField = addTextField(pane, "Display name: ", npc.name, npc.writable, listener);
+ nameField = addTranslatableTextField(pane, "Display name: ", npc.name, npc.writable, listener);
spawnGroupField = addTextField(pane, "Spawn group ID: ", npc.spawngroup_id, npc.writable, listener);
experienceField = addIntegerField(pane, "Experience reward: ", npc.getMonsterExperience(), false, false, listener);
dialogueBox = addDialogueBox(pane, npc.getProject(), "Initial phrase: ", npc.dialogue, npc.writable, listener);
diff --git a/src/com/gpl/rpg/atcontentstudio/ui/gamedataeditors/QuestEditor.java b/src/com/gpl/rpg/atcontentstudio/ui/gamedataeditors/QuestEditor.java
index abb6588..bfe20a0 100644
--- a/src/com/gpl/rpg/atcontentstudio/ui/gamedataeditors/QuestEditor.java
+++ b/src/com/gpl/rpg/atcontentstudio/ui/gamedataeditors/QuestEditor.java
@@ -69,7 +69,7 @@ public class QuestEditor extends JSONElementEditor {
idField = addTextField(pane, "Internal ID: ", quest.id, quest.writable, listener);
- nameField = addTextField(pane, "Quest Name: ", quest.name, quest.writable, listener);
+ nameField = addTranslatableTextField(pane, "Quest Name: ", quest.name, quest.writable, listener);
visibleBox = addIntegerBasedCheckBox(pane, "Visible in quest log", quest.visible_in_log, quest.writable, listener);
JPanel stagesPane = new JPanel();
diff --git a/src/com/gpl/rpg/atcontentstudio/utils/HashUtils.java b/src/com/gpl/rpg/atcontentstudio/utils/HashUtils.java
new file mode 100644
index 0000000..e044803
--- /dev/null
+++ b/src/com/gpl/rpg/atcontentstudio/utils/HashUtils.java
@@ -0,0 +1,60 @@
+package com.gpl.rpg.atcontentstudio.utils;
+
+import java.io.UnsupportedEncodingException;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import com.zackehh.siphash.SipHash;
+import com.zackehh.siphash.SipHashResult;
+
+public class HashUtils {
+
+ private static final String WEBLATE_SIPASH_KEY = "Weblate Sip Hash";
+
+ private static final Map HASHER_CACHE = new LinkedHashMap();
+
+ public static String weblateHash(String str, String ctx) {
+
+ byte[] data = null;
+
+ if (str != null) {
+ byte[] strBytes;
+ try {
+ strBytes = str.getBytes("UTF-8");
+ byte[] ctxBytes = ctx.getBytes("UTF-8");
+ data = new byte[strBytes.length + ctxBytes.length];
+ System.arraycopy(strBytes, 0, data, 0, strBytes.length);
+ System.arraycopy(ctxBytes, 0, data, strBytes.length, ctxBytes.length);
+ } catch (UnsupportedEncodingException e) {
+ e.printStackTrace();
+ }
+
+ } else {
+ try {
+ data = ctx.getBytes("UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ e.printStackTrace();
+ }
+ }
+
+ return siphash(WEBLATE_SIPASH_KEY, data);
+ }
+
+ private static String siphash(String key, byte[] data) {
+ SipHash hasher = HASHER_CACHE.get(key);
+ if (hasher == null) {
+ hasher= new SipHash("Weblate Sip Hash".getBytes());
+ HASHER_CACHE.put(key, hasher);
+ }
+
+ if (data != null) {
+ SipHashResult result = hasher.hash(data);
+ return result.getHex();
+ }
+
+
+ return null;
+
+ }
+
+}