From 0a7cb40dbceaae9b30ba77d07ad66a4f7798fd5a Mon Sep 17 00:00:00 2001 From: Zukero Date: Mon, 10 Apr 2017 18:16:17 +0200 Subject: [PATCH] Added link to Weblate from translatable strings once the translator mode is activated in the workspace settings. This required the addition of a new lib (in source form) for a SipHash implementation. --- .classpath | 1 + res/LICENSE.siphash-zackehh.txt | 21 ++ siphash-zackehh/.gitignore | 19 ++ siphash-zackehh/.travis.yml | 3 + siphash-zackehh/LICENSE | 21 ++ siphash-zackehh/README.md | 77 ++++++ siphash-zackehh/pom.xml | 171 +++++++++++++ .../java/com/zackehh/siphash/SipHash.java | 106 +++++++++ .../java/com/zackehh/siphash/SipHashCase.java | 8 + .../com/zackehh/siphash/SipHashConstants.java | 63 +++++ .../com/zackehh/siphash/SipHashDigest.java | 225 ++++++++++++++++++ .../java/com/zackehh/siphash/SipHashKey.java | 53 +++++ .../com/zackehh/siphash/SipHashResult.java | 121 ++++++++++ .../com/zackehh/siphash/SipHashCaseTest.java | 27 +++ .../zackehh/siphash/SipHashConstantsTest.java | 32 +++ .../zackehh/siphash/SipHashDigestTest.java | 148 ++++++++++++ .../com/zackehh/siphash/SipHashKeyTest.java | 27 +++ .../java/com/zackehh/siphash/SipHashTest.java | 107 +++++++++ .../com/zackehh/siphash/SipHashTestUtils.java | 13 + .../rpg/atcontentstudio/ATContentStudio.java | 2 + .../model/WorkspaceSettings.java | 18 ++ .../rpg/atcontentstudio/ui/AboutEditor.java | 4 + .../gpl/rpg/atcontentstudio/ui/Editor.java | 104 ++++++++ .../ui/WorkspaceSettingsEditor.java | 48 +++- .../gamedataeditors/ActorConditionEditor.java | 2 +- .../ui/gamedataeditors/DialogueEditor.java | 2 +- .../gamedataeditors/ItemCategoryEditor.java | 2 +- .../ui/gamedataeditors/ItemEditor.java | 4 +- .../ui/gamedataeditors/NPCEditor.java | 2 +- .../ui/gamedataeditors/QuestEditor.java | 2 +- .../rpg/atcontentstudio/utils/HashUtils.java | 60 +++++ 31 files changed, 1485 insertions(+), 8 deletions(-) create mode 100644 res/LICENSE.siphash-zackehh.txt create mode 100644 siphash-zackehh/.gitignore create mode 100644 siphash-zackehh/.travis.yml create mode 100644 siphash-zackehh/LICENSE create mode 100644 siphash-zackehh/README.md create mode 100644 siphash-zackehh/pom.xml create mode 100644 siphash-zackehh/src/main/java/com/zackehh/siphash/SipHash.java create mode 100644 siphash-zackehh/src/main/java/com/zackehh/siphash/SipHashCase.java create mode 100644 siphash-zackehh/src/main/java/com/zackehh/siphash/SipHashConstants.java create mode 100644 siphash-zackehh/src/main/java/com/zackehh/siphash/SipHashDigest.java create mode 100644 siphash-zackehh/src/main/java/com/zackehh/siphash/SipHashKey.java create mode 100644 siphash-zackehh/src/main/java/com/zackehh/siphash/SipHashResult.java create mode 100644 siphash-zackehh/src/test/java/com/zackehh/siphash/SipHashCaseTest.java create mode 100644 siphash-zackehh/src/test/java/com/zackehh/siphash/SipHashConstantsTest.java create mode 100644 siphash-zackehh/src/test/java/com/zackehh/siphash/SipHashDigestTest.java create mode 100644 siphash-zackehh/src/test/java/com/zackehh/siphash/SipHashKeyTest.java create mode 100644 siphash-zackehh/src/test/java/com/zackehh/siphash/SipHashTest.java create mode 100644 siphash-zackehh/src/test/java/com/zackehh/siphash/SipHashTestUtils.java create mode 100644 src/com/gpl/rpg/atcontentstudio/utils/HashUtils.java 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 + +[![Build Status](https://travis-ci.org/zackehh/siphash-java.svg?branch=master)](https://travis-ci.org/zackehh/siphash-java) [![Coverage Status](https://coveralls.io/repos/zackehh/siphash-java/badge.svg?branch=master&service=github)](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; + + } + +}