diff --git a/data/src/main/java/io/github/adamw7/tools/data/source/file/ToonSyntax.java b/data/src/main/java/io/github/adamw7/tools/data/source/file/ToonSyntax.java index 96437e17..63dc2280 100644 --- a/data/src/main/java/io/github/adamw7/tools/data/source/file/ToonSyntax.java +++ b/data/src/main/java/io/github/adamw7/tools/data/source/file/ToonSyntax.java @@ -135,11 +135,41 @@ private static boolean isQuotedString(String value) { } private static String processEscapeSequences(String value) { - return value - .replace("\\\"", "\"") - .replace("\\\\", "\\") - .replace("\\n", "\n") - .replace("\\r", "\r") - .replace("\\t", "\t"); + StringBuilder result = new StringBuilder(value.length()); + int i = 0; + while (i < value.length()) { + i = appendChar(result, value, i); + } + return result.toString(); + } + + private static int appendChar(StringBuilder result, String value, int i) { + char c = value.charAt(i); + if (c != '\\' || i + 1 >= value.length()) { + result.append(c); + return i + 1; + } + return appendEscaped(result, value.charAt(i + 1), i); + } + + private static int appendEscaped(StringBuilder result, char escaped, int i) { + String replacement = replacementFor(escaped); + if (replacement == null) { + result.append('\\'); + return i + 1; + } + result.append(replacement); + return i + 2; + } + + private static String replacementFor(char escaped) { + return switch (escaped) { + case '"' -> "\""; + case '\\' -> "\\"; + case 'n' -> "\n"; + case 'r' -> "\r"; + case 't' -> "\t"; + default -> null; + }; } } diff --git a/data/src/main/java/io/github/adamw7/tools/data/structure/internal/DoubleHashing.java b/data/src/main/java/io/github/adamw7/tools/data/structure/internal/DoubleHashing.java index 6bf0ce12..93a9f3e2 100644 --- a/data/src/main/java/io/github/adamw7/tools/data/structure/internal/DoubleHashing.java +++ b/data/src/main/java/io/github/adamw7/tools/data/structure/internal/DoubleHashing.java @@ -43,10 +43,18 @@ public static int grownSize(int currentLength) { return Math.max((int) (currentLength * MULTIPLIER), currentLength + 1); } - /** The slot index probed on the given {@code iteration} of the sequence. */ + /** + * The slot index probed on the given {@code iteration} of the sequence. + * + *
The step {@code h2} is derived from {@code Math.abs(hashCode % (length - 1))}
+ * rather than {@code Math.abs(hashCode) % (length - 1)}: the two agree for every
+ * {@code hashCode} except {@link Integer#MIN_VALUE}, whose {@code Math.abs} stays
+ * negative and would otherwise yield a non-positive step that folds the probe
+ * sequence back onto a handful of slots.
+ */
public static int probe(int hashCode, int prime, int length, int iteration) {
int h1 = prime - (hashCode % prime);
- int h2 = 1 + (Math.abs(hashCode) % (length - 1));
+ int h2 = 1 + Math.abs(hashCode % (length - 1));
return Math.abs((h1 + (iteration * h2)) % length);
}
}
diff --git a/data/src/test/java/io/github/adamw7/tools/data/source/file/ToonSyntaxTest.java b/data/src/test/java/io/github/adamw7/tools/data/source/file/ToonSyntaxTest.java
index a81314ba..610e1cae 100644
--- a/data/src/test/java/io/github/adamw7/tools/data/source/file/ToonSyntaxTest.java
+++ b/data/src/test/java/io/github/adamw7/tools/data/source/file/ToonSyntaxTest.java
@@ -107,6 +107,19 @@ public void unquoteResolvesEscapeSequences() {
assertEquals("line\nbreak", ToonSyntax.unquote("\"line\\nbreak\""));
}
+ @Test
+ public void unquoteKeepsEscapedBackslashLiteralBeforeControlLetter() {
+ // The raw value is a \ \ n b : an escaped backslash followed by a literal
+ // 'n', so it must decode to a backslash and an 'n', not to a newline.
+ assertEquals("a\\nb", ToonSyntax.unquote("\"a\\\\nb\""));
+ assertEquals("a\\tb", ToonSyntax.unquote("\"a\\\\tb\""));
+ }
+
+ @Test
+ public void unquoteLeavesUnknownEscapeUntouched() {
+ assertEquals("a\\xb", ToonSyntax.unquote("\"a\\xb\""));
+ }
+
@Test
public void keyValuePatternMatchesDottedKeys() {
Matcher matcher = ToonSyntax.KEY_VALUE_PATTERN.matcher("user.name: Alice");
diff --git a/data/src/test/java/io/github/adamw7/tools/data/structure/DoubleHashingTest.java b/data/src/test/java/io/github/adamw7/tools/data/structure/DoubleHashingTest.java
index 8f018243..5d6c4d8f 100644
--- a/data/src/test/java/io/github/adamw7/tools/data/structure/DoubleHashingTest.java
+++ b/data/src/test/java/io/github/adamw7/tools/data/structure/DoubleHashingTest.java
@@ -4,6 +4,9 @@
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import java.util.HashSet;
+import java.util.Set;
+
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.function.Executable;
@@ -79,4 +82,24 @@ private void assertProbesAreValidIndices(int hashCode, int prime, int length) {
"index " + index + " out of range for hashCode " + hashCode);
}
}
+
+ @Test
+ public void probeVisitsEverySlotForMinValueHashCode() {
+ // Integer.MIN_VALUE is the one hashCode whose Math.abs stays negative. A prior
+ // step derived from Math.abs(hashCode) went non-positive for it, folding the
+ // sequence back onto a subset of the slots (12 of 13 here). On a prime-sized
+ // table the sequence must be a full permutation of every slot.
+ int length = 13;
+ int prime = 11;
+ assertProbesAllDistinct(Integer.MIN_VALUE, prime, length);
+ }
+
+ private void assertProbesAllDistinct(int hashCode, int prime, int length) {
+ Set