From ff235e4244b24e71b4086ae0a7c5db1726cda175 Mon Sep 17 00:00:00 2001 From: Artur Havliukovskyi Date: Thu, 19 Sep 2024 21:56:48 +0200 Subject: [PATCH] Implement radix-trie --- build.gradle.kts | 7 +- .../blaauwendraad/masker/json/KeyMatcher.java | 429 ++++++++++++------ .../masker/json/MaskingState.java | 10 +- .../json/InstanceCreationMemoryUsageTest.java | 2 +- .../masker/json/JsonMaskerTestUtil.java | 4 +- .../masker/json/KeyMatcherTest.java | 34 +- .../masker/json/MaskingStateTest.java | 4 +- 7 files changed, 324 insertions(+), 166 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 5b8510f4..25ce2375 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -174,12 +174,17 @@ tasks { "RedundantOverride", "RedundantThrows", "RemoveUnusedImports", + "DefaultCharset", "UnnecessarilyFullyQualified", "UnnecessarilyUsedValue", "UnnecessaryBoxedAssignment", "UnnecessaryBoxedVariable", "UnnecessaryFinal", "UnusedException", + "UnusedLabel", + "UnusedMethod", + "UnusedNestedClass", + "UnusedVariable", "WildcardImport", ) disable( @@ -208,4 +213,4 @@ tasks { withType { dependsOn(jacocoTestReport) } -} \ No newline at end of file +} diff --git a/src/main/java/dev/blaauwendraad/masker/json/KeyMatcher.java b/src/main/java/dev/blaauwendraad/masker/json/KeyMatcher.java index 3ae45f49..4712399a 100644 --- a/src/main/java/dev/blaauwendraad/masker/json/KeyMatcher.java +++ b/src/main/java/dev/blaauwendraad/masker/json/KeyMatcher.java @@ -6,9 +6,9 @@ import org.jspecify.annotations.Nullable; import java.nio.charset.StandardCharsets; -import java.util.ArrayDeque; -import java.util.Deque; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.TreeMap; @@ -46,6 +46,7 @@ final class KeyMatcher { private static final int SKIP_KEY_LOOKUP = -1; private final JsonMaskingConfig maskingConfig; private final TrieNode root; + private final StatefulRadixTrieNode statefulRoot; public KeyMatcher(JsonMaskingConfig maskingConfig) { this.maskingConfig = maskingConfig; @@ -57,71 +58,78 @@ public KeyMatcher(JsonMaskingConfig maskingConfig) { // see ByteTrie#insert documentation for more details maskingConfig.getKeyConfigs().keySet().forEach(key -> insert(preInitRootNode, key, true)); } - this.root = transform(preInitRootNode); + this.root = compress(preInitRootNode); + this.statefulRoot = new StatefulRadixTrieNode(root); } /** - * Transforms a (temporary) pre-initialization node into a permanent {@link KeyMatcher} look-up - * trie node. This is done by applying transformations of each (child) node starting from the - * root pre-init node and following a BFS order subsequently. + * Compresses a pre-initialization trie node into a permanent {@link KeyMatcher} radix trie + * node. Compression occurs only when a node has multiple children sharing a longest common + * prefix. In such cases, the common prefix is merged into a continuous prefix to reduce memory + * usage and optimize the lookups. * - * @param preInitNode the node which will be transformed by having all its children transformed - * @return the transformed pre-initialization trie into a post-initialization trie + * @param node the node to be compressed + * @return the compressed pre-initialization trie into a post-initialization trie */ - static TrieNode transform(PreInitTrieNode preInitNode) { - Map transformedNodes = new HashMap<>(); - Deque stack = new ArrayDeque<>(); - stack.push(preInitNode); - while (!stack.isEmpty()) { - PreInitTrieNode currentPreInitNode = stack.pop(); - if (transformedNodes.containsKey(currentPreInitNode)) { - // lower-case and upper-case children represented by the same exact node under a different index - // avoid transforming the children that were already transformed - continue; + static TrieNode compress(PreInitTrieNode node) { + List commonPrefix = new ArrayList<>(); + while (true) { + if (node.endOfWord || node.children.size() != 1) { + return convertToRadixNode(node, commonPrefix); } - int childrenArrayOffset = -1; - int childrenArraySize = 0; - int childrenUpperArrayOffset = -1; - int childrenUpperArraySize = 0; - if (!currentPreInitNode.children.isEmpty()) { - childrenArrayOffset = currentPreInitNode.children.firstKey(); - childrenArraySize = currentPreInitNode.children.lastKey() - childrenArrayOffset + 1; - if (!currentPreInitNode.childrenUpper.isEmpty()) { - childrenUpperArrayOffset = currentPreInitNode.childrenUpper.firstKey(); - childrenUpperArraySize = currentPreInitNode.childrenUpper.lastKey() - childrenUpperArrayOffset + 1; - } + var childBytes = new byte[2]; + commonPrefix.add(childBytes); + childBytes[0] = node.children.firstKey(); + if (!node.childrenUpper.isEmpty()) { + childBytes[1] = node.childrenUpper.firstKey(); } - TrieNode currentNode = - new TrieNode( - childrenArrayOffset, - childrenUpperArrayOffset, - childrenArraySize == 0 ? TrieNode.EMPTY_CHILDREN : new TrieNode[childrenArraySize], - childrenUpperArraySize == 0 - ? TrieNode.EMPTY_CHILDREN - : new TrieNode[childrenUpperArraySize], - currentPreInitNode.keyMaskingConfig, - currentPreInitNode.endOfWord, - currentPreInitNode.negativeMatch); - transformedNodes.put(currentPreInitNode, currentNode); - stack.addAll(currentPreInitNode.children.values()); - stack.addAll(currentPreInitNode.childrenUpper.values()); + + node = node.children.firstEntry().getValue(); } + } - for (Map.Entry entry : transformedNodes.entrySet()) { - PreInitTrieNode currentPreInitNode = entry.getKey(); - TrieNode currentNode = entry.getValue(); - - currentPreInitNode.children.forEach( - (byteValue, childNode) -> - currentNode.children[byteValue - currentNode.childrenArrayOffset] = - transformedNodes.get(childNode)); - currentPreInitNode.childrenUpper.forEach( - (byteValue, childNode) -> - currentNode.childrenUpper[byteValue - currentNode.childrenUpperArrayOffset] = - transformedNodes.get(childNode)); + private static TrieNode convertToRadixNode(PreInitTrieNode node, List commonPrefix) { + // reached the end of prefix, create a new node + byte[] prefix = new byte[commonPrefix.size()]; + byte[] prefixUpper = new byte[commonPrefix.size()]; + for (int i = 0; i < commonPrefix.size(); i++) { + byte[] prefixes = commonPrefix.get(i); + prefix[i] = prefixes[0]; + prefixUpper[i] = prefixes[1]; } - return Objects.requireNonNull(transformedNodes.get(preInitNode)); + TrieNode radixNode = new TrieNode(prefix, prefixUpper); + radixNode.endOfWord = node.endOfWord; + radixNode.negativeMatch = node.negativeMatch; + radixNode.keyMaskingConfig = node.keyMaskingConfig; + if (!node.children.isEmpty()) { + Map transformedNodes = new HashMap<>(); + int childrenArrayOffset = node.children.firstKey(); + int childrenArraySize = node.children.lastKey() - childrenArrayOffset + 1; + int childrenUpperArrayOffset = -1; + int childrenUpperArraySize = 0; + if (!node.childrenUpper.isEmpty()) { + childrenUpperArrayOffset = node.childrenUpper.firstKey(); + childrenUpperArraySize = node.childrenUpper.lastKey() - childrenUpperArrayOffset + 1; + } + radixNode.childrenArrayOffset = childrenArrayOffset; + radixNode.children = new TrieNode[childrenArraySize]; + radixNode.childrenUpperArrayOffset = childrenUpperArrayOffset; + radixNode.childrenUpper = new TrieNode[childrenUpperArraySize]; + + for (Map.Entry e : node.children.entrySet()) { + byte b = e.getKey(); + var child = e.getValue(); + radixNode.children[b - radixNode.childrenArrayOffset] = transformedNodes.computeIfAbsent(child, KeyMatcher::compress); + } + + for (Map.Entry e : node.childrenUpper.entrySet()) { + byte b = e.getKey(); + var child = e.getValue(); + radixNode.childrenUpper[b - radixNode.childrenUpperArrayOffset] = transformedNodes.computeIfAbsent(child, KeyMatcher::compress); + } + } + return radixNode; } /** @@ -210,44 +218,53 @@ private void insert(PreInitTrieNode node, String word, boolean negativeMatch) { * masked */ @Nullable KeyMaskingConfig getMaskConfigIfMatched( - byte[] bytes, int keyOffset, int keyLength, @Nullable TrieNode currentJsonPathNode) { - // first search by key - TrieNode node = currentJsonPathNode; - if (maskingConfig.isInMaskMode()) { - // check JSONPath first, as it's more specific - // if found - mask with this config - // if not found - do not mask - if (node != null && node.endOfWord) { - return node.keyMaskingConfig; - } else if (keyLength != SKIP_KEY_LOOKUP) { - // also check regular key - node = searchNode(root, bytes, keyOffset, keyLength); - if (node != null && node.endOfWord) { - return node.keyMaskingConfig; - } + byte[] bytes, int keyOffset, int keyLength, @Nullable StatefulRadixTrieNode currentJsonPathNode) { + try { + if (currentJsonPathNode != null) { + currentJsonPathNode.checkpoint(); } - return null; - } else { - // check JSONPath first, as it's more specific - // if found and is not negativeMatch - do not mask - // if found and is negative match - mask, but with a specific config - // if not found - mask with default config - if (node != null && node.endOfWord) { - if (node.negativeMatch) { - return node.keyMaskingConfig; + StatefulRadixTrieNode node = currentJsonPathNode; + if (maskingConfig.isInMaskMode()) { + // check JSONPath first, as it's more specific + // if found - mask with this config + // if not found - do not mask + if (node != null && node.endOfWord()) { + return node.keyMaskingConfig(); + } else if (keyLength != SKIP_KEY_LOOKUP) { + // also check regular key + node = searchNode(statefulRoot, bytes, keyOffset, keyLength); + if (node != null && node.endOfWord()) { + return node.keyMaskingConfig(); + } } return null; - } else if (keyLength != SKIP_KEY_LOOKUP) { - // also check regular key - node = searchNode(root, bytes, keyOffset, keyLength); - if (node != null && node.endOfWord) { - if (node.negativeMatch) { - return node.keyMaskingConfig; + } else { + // check JSONPath first, as it's more specific + // if found and is not negativeMatch - do not mask + // if found and is negative match - mask, but with a specific config + // if not found - mask with default config + if (node != null && node.endOfWord()) { + if (node.negativeMatch()) { + return node.keyMaskingConfig(); } return null; + } else if (keyLength != SKIP_KEY_LOOKUP) { + // also check regular key + node = searchNode(statefulRoot, bytes, keyOffset, keyLength); + if (node != null && node.endOfWord()) { + if (node.negativeMatch()) { + return node.keyMaskingConfig(); + } + return null; + } } + return maskingConfig.getDefaultConfig(); } - return maskingConfig.getDefaultConfig(); + } finally { + if (currentJsonPathNode != null) { + currentJsonPathNode.restore(); + } + statefulRoot.restore(); } } @@ -262,8 +279,8 @@ private void insert(PreInitTrieNode node, String word, boolean negativeMatch) { * @return the node if found, {@code null} otherwise. */ @Nullable - private TrieNode searchNode(TrieNode from, byte[] bytes, int offset, int length) { - TrieNode node = from; + private StatefulRadixTrieNode searchNode(StatefulRadixTrieNode from, byte[] bytes, int offset, int length) { + var node = from; int endIndex = offset + length; for (int i = offset; i < endIndex; i++) { @@ -352,8 +369,8 @@ private static boolean isEncodedCharacter(byte[] bytes, int fromIndex, int toInd return fromIndex <= toIndex - 6 && bytes[fromIndex] == '\\' && bytes[fromIndex + 1] == 'u'; } - @Nullable TrieNode getJsonPathRootNode() { - return root.child((byte) '$'); + @Nullable StatefulRadixTrieNode getJsonPathRootNode() { + return new StatefulRadixTrieNode(root).child((byte) '$'); } /** @@ -368,20 +385,33 @@ private static boolean isEncodedCharacter(byte[] bytes, int fromIndex, int toInd * @return a TrieNode of the last symbol of the segment. {@code null} if the segment is not in * the trie. */ - @Nullable TrieNode traverseJsonPathSegment( - byte[] bytes, @Nullable TrieNode begin, int keyOffset, int keyLength) { + @Nullable StatefulRadixTrieNode traverseJsonPathSegment( + byte[] bytes, @Nullable StatefulRadixTrieNode begin, int keyOffset, int keyLength) { if (begin == null) { return null; } - TrieNode current = begin.child((byte) '.'); - if (current == null) { + try { + var current = begin.child((byte) '.'); + if (current == null) { + return null; + } + if (current.isJsonPathWildcard()) { + current.child((byte) '*'); + return new StatefulRadixTrieNode(current); + } + + current = searchNode(current, bytes, keyOffset, keyLength); + if (current != null) { + return new StatefulRadixTrieNode(current); + } return null; + } finally { + begin.restore(); } - TrieNode wildcardLookAhead = current.child((byte) '*'); - if (wildcardLookAhead != null && (wildcardLookAhead.endOfWord || wildcardLookAhead.child((byte) '.') != null)) { - return wildcardLookAhead; - } - return searchNode(current, bytes, keyOffset, keyLength); + } + + public String printTree() { + return root.toString(); } /** @@ -389,6 +419,12 @@ private static boolean isEncodedCharacter(byte[] bytes, int fromIndex, int toInd * represents a single character). An array is used instead of a Map for instant access without * type casts. * + *

Unlike a regular trie, a radix trie is compressed by merging all nodes that share a common + * prefix, reducing both memory usage and access time. As a result, each match becomes a stateful + * operation that requires not only identifying the next child node but also tracking the sequence + * (i.e., the number of children already matched). Naturally, after each match, the sequence must + * be incremented, or if a different node is encountered, it should be reset. + * *

The array starts from a non-null child which represents the byte with value {@link * TrieNode#childrenArrayOffset} and every subsequent byte is offset by that value. * @@ -399,66 +435,88 @@ private static boolean isEncodedCharacter(byte[] bytes, int fromIndex, int toInd * 1, while storing them in the same array would result in a gap of 32 {@code null}-elements. */ static class TrieNode { - private static final TrieNode[] EMPTY_CHILDREN = new TrieNode[0]; - + public static final TrieNode[] EMPTY = new TrieNode[0]; + final byte[] prefix; + final byte[] prefixUpper; + @Nullable + TrieNode[] children = EMPTY; + @Nullable + TrieNode[] childrenUpper = EMPTY; + int childrenArrayOffset = -1; + int childrenUpperArrayOffset = -1; /** - * Indicates the indexing offset of the children array. So let's say this value is 65 (ASCII - * 'A'), then 0th index represents this byte and the 20th index in the array would represent - * the byte value 85 (ASCII 'U'). This is essentially a memory optimization to not store 256 - * references for the children, but much less in most practical cases at the cost of storing - * the offset itself (4 bytes). + * Masking configuration for the key that ends at this node. */ - private final int childrenArrayOffset; - - private final int childrenUpperArrayOffset; - - @Nullable TrieNode[] children; - @Nullable TrieNode[] childrenUpper; - - /** Masking configuration for the key that ends at this node. */ - private final @Nullable KeyMaskingConfig keyMaskingConfig; - - /** A marker that the character indicates that the key ends at this node. */ - private final boolean endOfWord; - + @Nullable + KeyMaskingConfig keyMaskingConfig = null; + /** + * A marker that the character indicates that the key ends at this node. + */ + boolean endOfWord = false; /** - * Used to store the configuration, but indicate that json-masker is in ALLOW mode and the - * key is not allowed. + * Used to store the configuration, but indicate that json-masker is in ALLOW mode and the key is not allowed. */ - private final boolean negativeMatch; - - TrieNode( - int childrenArrayOffset, - int childrenUpperArrayOffset, - TrieNode[] children, - TrieNode[] childrenUpper, - @Nullable KeyMaskingConfig keyMaskingConfig, - boolean endOfWord, - boolean negativeMatch) { - this.childrenArrayOffset = childrenArrayOffset; - this.childrenUpperArrayOffset = childrenUpperArrayOffset; - this.children = children; - this.childrenUpper = childrenUpper; - this.keyMaskingConfig = keyMaskingConfig; - this.endOfWord = endOfWord; - this.negativeMatch = negativeMatch; + boolean negativeMatch = false; + + TrieNode(byte[] prefix, byte[] prefixUpper) { + this.prefix = prefix; + this.prefixUpper = prefixUpper; } /** - * Retrieves a child node by the byte value. Returns {@code null}, if the trie has no - * matches. + * Retrieves a child node by the byte value. Returns {@code null}, if the trie has no matches. */ - @Nullable TrieNode child(byte b) { - int offsetIndex = b - childrenArrayOffset; - // This Sonar/IntelliJ warning on the next line is incorrect because the NullAway bug - if (offsetIndex >= 0 && offsetIndex < children.length && children[offsetIndex] != null) { - return children[offsetIndex]; + @Nullable TrieNode child(byte b, int sequence) { + if (sequence == prefix.length) { + int offsetIndex = b - childrenArrayOffset; + TrieNode child = null; + if (offsetIndex >= 0 && offsetIndex < children.length) { + child = children[offsetIndex]; + } + int offsetUpperIndex = b - childrenUpperArrayOffset; + if (offsetUpperIndex >= 0 && offsetUpperIndex < childrenUpper.length) { + child = childrenUpper[offsetUpperIndex]; + } + return child; + } else if (prefix[sequence] == b || prefixUpper[sequence] == b) { + return this; + } else { + return null; } - int offsetUpperIndex = b - childrenUpperArrayOffset; - if (offsetUpperIndex >= 0 && offsetUpperIndex < childrenUpper.length) { - return childrenUpper[offsetUpperIndex]; + } + + boolean endOfWord(int sequence) { + return sequence == prefix.length && endOfWord; + } + + @Override + public String toString() { + return toString(0); + } + + public String toString(int indent) { + StringBuilder sb = new StringBuilder(); + sb.append(new String(prefix, StandardCharsets.UTF_8)); + int childrenIndent = indent + prefix.length; + boolean first = true; + for (int i = 0; i < children.length; i++) { + TrieNode child = children[i]; + if (child != null) { + if (!first) { + sb.append("\n"); + sb.append(" ".repeat(childrenIndent)); + } + String prefix = " -> " + (char) (i + childrenArrayOffset); + sb.append(prefix); + sb.append(child.toString(childrenIndent + prefix.length())); + first = false; + } } - return null; + // only for root + if (indent == 0) { + sb.append("\n"); + } + return sb.toString(); } } @@ -493,7 +551,7 @@ static class PreInitTrieNode { boolean negativeMatch = false; /** - * @see TrieNode#child(byte) + * @see TrieNode#child(byte, int) */ @Nullable PreInitTrieNode child(byte b) { PreInitTrieNode child = children.get(b); @@ -520,4 +578,73 @@ void addUpper(Byte b, PreInitTrieNode child) { childrenUpper.put(b, child); } } + + /** + * A helper class for managing (radix) trie node and the sequence. On top of regular methods + * used for matching, also contains {@link #checkpoint()} and {@link #restore()} to allow + * repeated matching that would otherwise pollute the sequence. + */ + static class StatefulRadixTrieNode { + private TrieNode node; + private int sequence; + + private TrieNode checkPointNode; + private int checkPointSequence; + + StatefulRadixTrieNode(TrieNode node) { + this.node = this.checkPointNode = node; + this.sequence = this.checkPointSequence = 0; + } + + StatefulRadixTrieNode(StatefulRadixTrieNode node) { + this.node = this.checkPointNode = node.node; + this.sequence = this.checkPointSequence = node.sequence; + } + + @Nullable + StatefulRadixTrieNode child(byte b) { + var child = node.child(b, sequence++); + if (child == null) { + return null; + } else if (child != node) { + node = child; + sequence = 0; + } + return this; + } + + boolean isJsonPathWildcard() { + return node.child((byte) '*', sequence) != null + && (node.endOfWord(sequence + 1) + || node.child((byte) '.', sequence + 1) != null); + } + + boolean endOfWord() { + return node.endOfWord(sequence); + } + + boolean negativeMatch() { + return node.negativeMatch; + } + + @Nullable + KeyMaskingConfig keyMaskingConfig() { + return node.keyMaskingConfig; + } + + void checkpoint() { + checkPointNode = node; + checkPointSequence = sequence; + } + + void restore() { + node = checkPointNode; + sequence = checkPointSequence; + } + + @Override + public String toString() { + return "[sequence: %s] %s".formatted(sequence, node); + } + } } diff --git a/src/main/java/dev/blaauwendraad/masker/json/MaskingState.java b/src/main/java/dev/blaauwendraad/masker/json/MaskingState.java index f9217ac1..09d2e37a 100644 --- a/src/main/java/dev/blaauwendraad/masker/json/MaskingState.java +++ b/src/main/java/dev/blaauwendraad/masker/json/MaskingState.java @@ -56,7 +56,7 @@ final class MaskingState implements ValueMaskerContext { * Current JSONPath is represented by a stack of segment references. * A stack is implemented with an array of the trie nodes that reference the end of the segment */ - private KeyMatcher.@Nullable TrieNode @Nullable [] currentJsonPath = null; + private KeyMatcher.@Nullable StatefulRadixTrieNode @Nullable [] currentJsonPath = null; private int currentJsonPathHeadIndex = -1; private int currentTokenStartIndex = -1; @@ -64,7 +64,7 @@ public MaskingState(byte[] message, boolean trackJsonPath) { this.message = message; this.messageLength = message.length; if (trackJsonPath) { - currentJsonPath = new KeyMatcher.TrieNode[INITIAL_JSONPATH_STACK_CAPACITY]; + currentJsonPath = new KeyMatcher.StatefulRadixTrieNode[INITIAL_JSONPATH_STACK_CAPACITY]; } this.inputStream = null; this.outputStream = null; @@ -87,7 +87,7 @@ public MaskingState(InputStream inputStream, OutputStream outputStream, boolean this.message = new byte[this.bufferSize]; this.messageLength = 0; if (trackJsonPath) { - currentJsonPath = new KeyMatcher.TrieNode[INITIAL_JSONPATH_STACK_CAPACITY]; + currentJsonPath = new KeyMatcher.StatefulRadixTrieNode[INITIAL_JSONPATH_STACK_CAPACITY]; } readNextBuffer(); } @@ -232,7 +232,7 @@ boolean jsonPathEnabled() { * * @param trieNode a node in the trie where the new segment ends. */ - void expandCurrentJsonPath(KeyMatcher.@Nullable TrieNode trieNode) { + void expandCurrentJsonPath(KeyMatcher.@Nullable StatefulRadixTrieNode trieNode) { if (currentJsonPath != null) { currentJsonPath[++currentJsonPathHeadIndex] = trieNode; if (currentJsonPathHeadIndex == currentJsonPath.length - 1) { @@ -254,7 +254,7 @@ void backtrackCurrentJsonPath() { /** * Returns the TrieNode that references the end of the latest segment in the current jsonpath */ - public KeyMatcher.@Nullable TrieNode getCurrentJsonPathNode() { + public KeyMatcher.@Nullable StatefulRadixTrieNode getCurrentJsonPathNode() { if (currentJsonPath != null && currentJsonPathHeadIndex != -1) { return currentJsonPath[currentJsonPathHeadIndex]; } else { diff --git a/src/test/java/dev/blaauwendraad/masker/json/InstanceCreationMemoryUsageTest.java b/src/test/java/dev/blaauwendraad/masker/json/InstanceCreationMemoryUsageTest.java index a5954a87..fb35ae7a 100644 --- a/src/test/java/dev/blaauwendraad/masker/json/InstanceCreationMemoryUsageTest.java +++ b/src/test/java/dev/blaauwendraad/masker/json/InstanceCreationMemoryUsageTest.java @@ -1623,7 +1623,7 @@ void realWorldObfuscatedKeysInstanceCreation() { long memoryBeforeInstanceCreationKb = getCurrentRetainedMemory(); - long memoryLimitKb = 2_000; + long memoryLimitKb = 700; long memoryConsumedKb = bytesToKb(memoryBeforeInstanceCreationKb - memoryBeforeInstanceCreation); Assertions.assertThat(memoryConsumedKb) diff --git a/src/test/java/dev/blaauwendraad/masker/json/JsonMaskerTestUtil.java b/src/test/java/dev/blaauwendraad/masker/json/JsonMaskerTestUtil.java index 423ec963..2ab7572c 100644 --- a/src/test/java/dev/blaauwendraad/masker/json/JsonMaskerTestUtil.java +++ b/src/test/java/dev/blaauwendraad/masker/json/JsonMaskerTestUtil.java @@ -39,8 +39,8 @@ public static List getJsonMaskerTestInstancesFromFile(St applyConfig(jsonMaskingConfig, builder); } JsonMaskingConfig maskingConfig = builder.build(); - var input = jsonNode.get("input").toString(); - var expectedOutput = jsonNode.get("expectedOutput").toString(); + var input = jsonNode.get("input").toPrettyString(); + var expectedOutput = jsonNode.get("expectedOutput").toPrettyString(); testInstances.add(new JsonMaskerTestInstance(input, expectedOutput, new KeyContainsMasker(maskingConfig))); } return testInstances; diff --git a/src/test/java/dev/blaauwendraad/masker/json/KeyMatcherTest.java b/src/test/java/dev/blaauwendraad/masker/json/KeyMatcherTest.java index 82b6db76..59b2800e 100644 --- a/src/test/java/dev/blaauwendraad/masker/json/KeyMatcherTest.java +++ b/src/test/java/dev/blaauwendraad/masker/json/KeyMatcherTest.java @@ -111,7 +111,7 @@ void shouldMatchJsonPaths() { """; byte[] bytes = json.getBytes(StandardCharsets.UTF_8); - KeyMatcher.TrieNode node = keyMatcher.getJsonPathRootNode(); + var node = keyMatcher.getJsonPathRootNode(); node = keyMatcher.traverseJsonPathSegment(bytes, node, indexOf(bytes, 'a'), 1); node = keyMatcher.traverseJsonPathSegment(bytes, node, indexOf(bytes, 'b'), 1); assertThat(keyMatcher.getMaskConfigIfMatched(bytes, 0, 0, node)).isNotNull(); @@ -150,7 +150,7 @@ void shouldMatchJsonPathArrays() { """; byte[] bytes = json.getBytes(StandardCharsets.UTF_8); - KeyMatcher.TrieNode node = keyMatcher.getJsonPathRootNode(); + var node = keyMatcher.getJsonPathRootNode(); node = keyMatcher.traverseJsonPathSegment(bytes, node, indexOf(bytes, 'a'), 1); node = keyMatcher.traverseJsonPathSegment(bytes, node, -1, -1); node = keyMatcher.traverseJsonPathSegment(bytes, node, indexOf(bytes, 'b'), 1); @@ -184,7 +184,7 @@ void shouldNotMatchJsonPathPrefix() { """; byte[] bytes = json.getBytes(StandardCharsets.UTF_8); - KeyMatcher.TrieNode node = keyMatcher.getJsonPathRootNode(); + var node = keyMatcher.getJsonPathRootNode(); node = keyMatcher.traverseJsonPathSegment(bytes, node, 2, 4); assertThat(keyMatcher.getMaskConfigIfMatched(bytes, 0, -1, node)).isNull(); @@ -206,7 +206,7 @@ void shouldReturnMaskingConfigForJsonPathInAllowMode() { """; byte[] bytes = json.getBytes(StandardCharsets.UTF_8); - KeyMatcher.TrieNode node = keyMatcher.getJsonPathRootNode(); + var node = keyMatcher.getJsonPathRootNode(); node = keyMatcher.traverseJsonPathSegment(bytes, node, 2, 7); assertThat(keyMatcher.getMaskConfigIfMatched(bytes, 0, -1, node)).isNull(); @@ -255,4 +255,30 @@ private int indexOf(byte[] bytes, char c) { } return found; } + + @Test + void printsNicely() { + JsonMaskingConfig config = JsonMaskingConfig.builder() + .allowKeys("romane", "romanus", "romulus", "rubens", "ruber", "rubicon", "rubicondus") + .build(); + KeyMatcher keyMatcher = new KeyMatcher(config); + assertThat(keyMatcher.printTree()) + .isEqualTo(""" + r -> om -> an -> e + -> us + -> ulus + -> ub -> e -> ns + -> r + -> icon -> dus + """); + } + + @Test + void printsEmpty() { + JsonMaskingConfig config = JsonMaskingConfig.builder() + .allowKeys() + .build(); + KeyMatcher keyMatcher = new KeyMatcher(config); + assertThat(keyMatcher.printTree()).isEqualTo("\n"); + } } diff --git a/src/test/java/dev/blaauwendraad/masker/json/MaskingStateTest.java b/src/test/java/dev/blaauwendraad/masker/json/MaskingStateTest.java index 0829b17a..52c925eb 100644 --- a/src/test/java/dev/blaauwendraad/masker/json/MaskingStateTest.java +++ b/src/test/java/dev/blaauwendraad/masker/json/MaskingStateTest.java @@ -47,7 +47,7 @@ void shouldReturnStringRepresentationForDebugging() { void jsonPathExceedsCapacity() { MaskingState maskingState = new MaskingState("[]".getBytes(StandardCharsets.UTF_8), true); for (int i = 0; i < 101; i++) { - maskingState.expandCurrentJsonPath(KeyMatcher.transform(new KeyMatcher.PreInitTrieNode())); + maskingState.expandCurrentJsonPath(new KeyMatcher.StatefulRadixTrieNode(KeyMatcher.compress(new KeyMatcher.PreInitTrieNode()))); } Assertions.assertThat(maskingState.getCurrentJsonPathNode()).isNotNull(); } @@ -91,4 +91,4 @@ void shouldUseCorrectOffsetWhenThrowingValueMaskerError() { .hasMessage("Didn't like the value at index 3 at index 19"); } -} \ No newline at end of file +}