org.owasp.fileio.codecs.HashTrie Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of java-file-io Show documentation
Show all versions of java-file-io Show documentation
The OWASP Java File I/O Security Project provides an easy to use library for validating and sanitizing filenames, directory paths, and uploaded files.
The newest version!
/**
* This file is part of the Open Web Application Security Project (OWASP) Java File IO Security project. For details, please see
* https://www.owasp.org/index.php/OWASP_Java_File_I_O_Security_Project.
*
* Copyright (c) 2014 - The OWASP Foundation
*
* This API is published by OWASP under the Apache 2.0 license. You should read and accept the LICENSE before you use, modify, and/or redistribute this software.
*
* @author Ed Schaller - Original ESAPI author
* @author August Detlefsen CodeMagi - Java File IO Security Project lead
* @created 2014
*/
package org.owasp.fileio.codecs;
import java.io.IOException;
import java.io.PushbackReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.owasp.fileio.util.NullSafe;
/**
* Trie implementation for CharSequence keys. This uses HashMaps for each level instead of the traditional array. This is done as with unicode, each level's array would be 64k entries.
*
* NOTE:
*
* - java.util.Map.remove(java.lang.Object) is not supported.
* -
* If deletion support is added the max key length will need work or removal.
*
* - Null values are not supported.
*
*
* @author Ed Schaller
*/
public class HashTrie implements Trie {
private static class Entry implements Map.Entry {
private CharSequence key;
private T value;
Entry(CharSequence key, T value) {
this.key = key;
this.value = value;
}
/**
* Convinence instantiator.
*
* @param key The key for the new instance
* @param keyLength The length of the key to use
* @param value The value for the new instance
* @return null if key or value is null new Entry(key,value) if {@link CharSequence#length()} == keyLength new Entry(key.subSequence(0,keyLength),value) otherwise
*/
static Entry newInstanceIfNeeded(CharSequence key, int keyLength, T value) {
if (value == null || key == null) {
return null;
}
if (key.length() > keyLength) {
key = key.subSequence(0, keyLength);
}
return new Entry(key, value);
}
/**
* Convinence instantiator.
*
* @param key The key for the new instance
* @param value The value for the new instance
* @return null if key or value is null new Entry(key,value) otherwise
*/
static Entry newInstanceIfNeeded(CharSequence key, T value) {
if (value == null || key == null) {
return null;
}
return new Entry(key, value);
}
/**
* **********
*/
/* Map.Entry */
/**
* **********
*/
public CharSequence getKey() {
return key;
}
public T getValue() {
return value;
}
public T setValue(T value) {
throw new UnsupportedOperationException();
}
/**
* *****************
*/
/* java.lang.Object */
/**
* *****************
*/
public boolean equals(Map.Entry other) {
return (NullSafe.equals(key, other.getKey()) && NullSafe.equals(value, other.getValue()));
}
@Override
public boolean equals(Object o) {
if (o instanceof Map.Entry) {
return equals((Map.Entry) o);
}
return false;
}
@Override
public int hashCode() {
return NullSafe.hashCode(key) ^ NullSafe.hashCode(value);
}
@Override
public String toString() {
return NullSafe.toString(key) + " => " + NullSafe.toString(value);
}
}
/**
* Node inside the trie.
*/
private static class Node {
private T value = null;
private Map> nextMap;
/**
* Create a new Map for a node level. This is here so that if the underlying * Map implmentation needs to be switched it is easily done.
*
* @return A new Map for use.
*/
private static Map> newNodeMap() {
return new HashMap>();
}
/**
* Create a new Map for a node level. This is here so that if the underlying * Map implmentation needs to be switched it is easily done.
*
* @param prev Pervious map to use to populate the new map.
* @return A new Map for use.
*/
private static Map> newNodeMap(Map> prev) {
return new HashMap>(prev);
}
/**
* Set the value for the key terminated at this node.
*
* @param value The value for this key.
*/
void setValue(T value) {
this.value = value;
}
/**
* Get the node for the specified character.
*
* @param ch The next character to look for.
* @return The node requested or null if it is not present.
*/
Node getNextNode(Character ch) {
if (nextMap == null) {
return null;
}
return nextMap.get(ch);
}
/**
* Recursively add a key.
*
* @param key The key being added.
* @param pos The position in key that is being handled at this level.
*/
T put(CharSequence key, int pos, T addValue) {
Node nextNode;
Character ch;
T old;
if (key.length() == pos) { // at terminating node
old = value;
setValue(addValue);
return old;
}
ch = key.charAt(pos);
if (nextMap == null) {
nextMap = newNodeMap();
nextNode = new Node();
nextMap.put(ch, nextNode);
} else if ((nextNode = nextMap.get(ch)) == null) {
nextNode = new Node();
nextMap.put(ch, nextNode);
}
return nextNode.put(key, pos + 1, addValue);
}
/**
* Recursively lookup a key's value.
*
* @param key The key being looked up.
* @param pos The position in the key that is being looked up at this level.
* @return The value assocatied with the key or null if none exists.
*/
T get(CharSequence key, int pos) {
Node nextNode;
if (key.length() <= pos) // <= instead of == just in case
{
return value; // no value is null which is also not found
}
if ((nextNode = getNextNode(key.charAt(pos))) == null) {
return null;
}
return nextNode.get(key, pos + 1);
}
/**
* Recursively lookup the longest key match.
*
* @param key The key being looked up.
* @param pos The position in the key that is being looked up at this level.
* @return The Entry assocatied with the longest key match or null if none exists.
*/
Entry getLongestMatch(CharSequence key, int pos) {
Node nextNode;
Entry ret;
if (key.length() <= pos) // <= instead of == just in case
{
return Entry.newInstanceIfNeeded(key, value);
}
if ((nextNode = getNextNode(key.charAt(pos))) == null) { // last in trie... return ourselves
return Entry.newInstanceIfNeeded(key, pos, value);
}
if ((ret = nextNode.getLongestMatch(key, pos + 1)) != null) {
return ret;
}
return Entry.newInstanceIfNeeded(key, pos, value);
}
/**
* Recursively lookup the longest key match.
*
* @param keyIn Where to read the key from
* @param pos The position in the key that is being looked up at this level.
* @return The Entry assocatied with the longest key match or null if none exists.
*/
Entry getLongestMatch(PushbackReader keyIn, StringBuilder key) throws IOException {
Node nextNode;
Entry ret;
int c;
char ch;
int prevLen;
// read next key char and append to key...
if ((c = keyIn.read()) < 0) // end of input, return what we have currently
{
return Entry.newInstanceIfNeeded(key, value);
}
ch = (char) c;
prevLen = key.length();
key.append(ch);
if ((nextNode = getNextNode(ch)) == null) { // last in trie... return ourselves
return Entry.newInstanceIfNeeded(key, value);
}
if ((ret = nextNode.getLongestMatch(keyIn, key)) != null) {
return ret;
}
// undo reading of key char and appending to key...
key.setLength(prevLen);
keyIn.unread(c);
return Entry.newInstanceIfNeeded(key, value);
}
/**
* Recursively rebuild the internal maps.
*/
void remap() {
if (nextMap == null) {
return;
}
nextMap = newNodeMap(nextMap);
for (Node node : nextMap.values()) {
node.remap();
}
}
/**
* Recursively search for a value.
*
* @param toFind The value to search for
* @return true if the value was found false otherwise
*/
boolean containsValue(Object toFind) {
if (value != null && toFind.equals(value)) {
return true;
}
if (nextMap == null) {
return false;
}
for (Node node : nextMap.values()) {
if (node.containsValue(toFind)) {
return true;
}
}
return false;
}
/**
* Recursively build values.
*
* @param values List being built.
* @return true if the value was found false otherwise
*/
Collection values(Collection values) {
if (value != null) {
values.add(value);
}
if (nextMap == null) {
return values;
}
for (Node node : nextMap.values()) {
node.values(values);
}
return values;
}
/**
* Recursively build a key set.
*
* @param key StringBuilder with our key.
* @param keys Set to add to
* @return keys with additions
*/
Set keySet(StringBuilder key, Set keys) {
int len = key.length();
if (value != null) // MUST toString here
{
keys.add(key.toString());
}
if (nextMap != null && nextMap.size() > 0) {
key.append('X');
for (Map.Entry> entry : nextMap.entrySet()) {
key.setCharAt(len, entry.getKey());
entry.getValue().keySet(key, keys);
}
key.setLength(len);
}
return keys;
}
/**
* Recursively build a entry set.
*
* @param key StringBuilder with our key.
* @param entries Set to add to
* @return entries with additions
*/
Set> entrySet(StringBuilder key, Set> entries) {
int len = key.length();
if (value != null) // MUST toString here
{
entries.add(new Entry(key.toString(), value));
}
if (nextMap != null && nextMap.size() > 0) {
key.append('X');
for (Map.Entry> entry : nextMap.entrySet()) {
key.setCharAt(len, entry.getKey());
entry.getValue().entrySet(key, entries);
}
key.setLength(len);
}
return entries;
}
}
private Node root;
private int maxKeyLen;
private int size;
public HashTrie() {
clear();
}
/**
* Get the key value entry who's key is the longest prefix match.
*
* @param key The key to lookup
* @return Entry with the longest matching key.
*/
@Override
public Map.Entry getLongestMatch(CharSequence key) {
if (root == null || key == null) {
return null;
}
return root.getLongestMatch(key, 0);
}
/**
* Get the key value entry who's key is the longest prefix match.
*
* @param keyIn Pushback reader to read the key from. This should have a buffer at least as large as {@link #getMaxKeyLength()} or an IOException may be thrown backing up.
* @return Entry with the longest matching key.
* @throws IOException if keyIn.read() or keyIn.unread() does.
*/
@Override
public Map.Entry getLongestMatch(PushbackReader keyIn) throws IOException {
if (root == null || keyIn == null) {
return null;
}
return root.getLongestMatch(keyIn, new StringBuilder());
}
/**
* Get the maximum key length.
*
* @return max key length.
*/
@Override
public int getMaxKeyLength() {
return maxKeyLen;
}
/**
* **************
*/
/* java.util.Map */
/**
* **************
*/
/**
* Clear all entries.
*/
@Override
public void clear() {
root = null;
maxKeyLen = -1;
size = 0;
}
/**
* {@inheritDoc}
*/
@Override
public boolean containsKey(Object key) {
return (get(key) != null);
}
/**
* {@inheritDoc}
*/
@Override
public boolean containsValue(Object value) {
if (root == null) {
return false;
}
return root.containsValue(value);
}
/**
* Add mapping.
*
* @param key The mapping's key.
* @param value value The mapping's value
* @throws NullPointerException if key or value is null.
*/
@Override
public T put(CharSequence key, T value) throws NullPointerException {
int len;
T old;
if (key == null) {
throw new NullPointerException("Null keys are not handled");
}
if (value == null) {
throw new NullPointerException("Null values are not handled");
}
if (root == null) {
root = new Node<>();
}
if ((old = root.put(key, 0, value)) != null) {
return old;
}
// after in case of replacement
if ((len = key.length()) > maxKeyLen) {
maxKeyLen = len;
}
size++;
return null;
}
/**
* Remove a entry.
*
* @return previous value
* @throws UnsupportedOperationException always.
*/
@Override
public T remove(Object key) throws UnsupportedOperationException {
throw new UnsupportedOperationException();
}
/**
* {@inheritDoc}
*/
@Override
public void putAll(Map extends CharSequence, ? extends T> map) {
for (Map.Entry extends CharSequence, ? extends T> entry : map.entrySet()) {
put(entry.getKey(), entry.getValue());
}
}
/**
* {@inheritDoc}
*/
@Override
public Set keySet() {
Set keys = new HashSet<>(size);
if (root == null) {
return keys;
}
return root.keySet(new StringBuilder(), keys);
}
/**
* {@inheritDoc}
*/
@Override
public Collection values() {
ArrayList values = new ArrayList<>(size());
if (root == null) {
return values;
}
return root.values(values);
}
/**
* {@inheritDoc}
*/
@Override
public Set> entrySet() {
Set> entries = new HashSet<>(size());
if (root == null) {
return entries;
}
return root.entrySet(new StringBuilder(), entries);
}
/**
* Get the value for a key.
*
* @param key The key to look up.
* @return The value for key or null if the key is not found.
*/
@Override
public T get(Object key) {
if (root == null || key == null) {
return null;
}
if (!(key instanceof CharSequence)) {
return null;
}
return root.get((CharSequence) key, 0);
}
/**
* Get the number of entries.
*
* @return the number or entries.
*/
@Override
public int size() {
return size;
}
/**
* {@inheritDoc}
*/
@Override
public boolean equals(Object other) {
if (other == null) {
return false;
}
if (!(other instanceof Map)) {
return false;
}
// per spec
return entrySet().equals(((Map) other).entrySet());
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
// per spec
return entrySet().hashCode();
}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
StringBuilder sb;
boolean first;
if (isEmpty()) {
return "{}";
}
sb = new StringBuilder();
first = true;
sb.append("{ ");
for (Map.Entry entry : entrySet()) {
if (first) {
first = false;
} else {
sb.append(", ");
}
sb.append(entry.toString());
}
sb.append(" }");
return sb.toString();
}
/**
* {@inheritDoc}
*/
@Override
public boolean isEmpty() {
return (size() == 0);
}
}