added a testcase and fixed Issue-918

This commit is contained in:
Marco Käferbeck
2022-04-22 12:26:32 +02:00
parent 6f35dcb785
commit 9e8ed84a37
2 changed files with 194 additions and 186 deletions

View File

@@ -38,7 +38,7 @@ public class MqttTopicTest {
String[][] matchingTopics = new String[][] { { "sport/tennis/player1/#", "sport/tennis/player1" }, String[][] matchingTopics = new String[][] { { "sport/tennis/player1/#", "sport/tennis/player1" },
{ "sport/tennis/player1/#", "sport/tennis/player1/ranking" }, { "sport/tennis/player1/#", "sport/tennis/player1/ranking" },
{ "sport/tennis/player1/#", "sport/tennis/player1/score/wimbledon" }, { "sport/#", "sport" }, { "sport/tennis/player1/#", "sport/tennis/player1/score/wimbledon" }, { "sport/#", "sport" },
{ "#", "sport/tennis/player1" } }; { "#", "sport/tennis/player1" } , {"sport/+/player1/ranking/#","sport/tennis/player1/ranking"} };
for (String[] pair : matchingTopics) { for (String[] pair : matchingTopics) {
Assert.assertTrue(pair[0] + " should match " + pair[1], MqttTopicValidator.isMatched(pair[0], pair[1])); Assert.assertTrue(pair[0] + " should match " + pair[1], MqttTopicValidator.isMatched(pair[0], pair[1]));

View File

@@ -4,214 +4,222 @@ import java.io.UnsupportedEncodingException;
public class MqttTopicValidator { public class MqttTopicValidator {
/** /**
* The forward slash (/) is used to separate each level within a topic tree and * The forward slash (/) is used to separate each level within a topic tree and provide a hierarchical structure to
* provide a hierarchical structure to the topic space. The use of the topic * the topic space. The use of the topic level separator is significant when the two wildcard characters are
* level separator is significant when the two wildcard characters are * encountered in topics specified by subscribers.
* encountered in topics specified by subscribers. */
*/ public static final String TOPIC_LEVEL_SEPARATOR = "/";
public static final String TOPIC_LEVEL_SEPARATOR = "/";
/** /**
* Multi-level wildcard The number sign (#) is a wildcard character that matches * Multi-level wildcard The number sign (#) is a wildcard character that matches any number of levels within a topic.
* any number of levels within a topic. */
*/ public static final String MULTI_LEVEL_WILDCARD = "#";
public static final String MULTI_LEVEL_WILDCARD = "#";
/** /**
* Single-level wildcard The plus sign (+) is a wildcard character that matches * Single-level wildcard The plus sign (+) is a wildcard character that matches only one topic level.
* only one topic level. */
*/ public static final String SINGLE_LEVEL_WILDCARD = "+";
public static final String SINGLE_LEVEL_WILDCARD = "+";
/** /**
* Multi-level wildcard pattern(/#) * Multi-level wildcard pattern(/#)
*/ */
public static final String MULTI_LEVEL_WILDCARD_PATTERN = TOPIC_LEVEL_SEPARATOR + MULTI_LEVEL_WILDCARD; public static final String MULTI_LEVEL_WILDCARD_PATTERN = TOPIC_LEVEL_SEPARATOR + MULTI_LEVEL_WILDCARD;
/** /**
* Topic wildcards (#+) * Topic wildcards (#+)
*/ */
public static final String TOPIC_WILDCARDS = MULTI_LEVEL_WILDCARD + SINGLE_LEVEL_WILDCARD; public static final String TOPIC_WILDCARDS = MULTI_LEVEL_WILDCARD + SINGLE_LEVEL_WILDCARD;
// topic name and topic filter length range defined in the spec // topic name and topic filter length range defined in the spec
private static final int MIN_TOPIC_LEN = 1; private static final int MIN_TOPIC_LEN = 1;
private static final int MAX_TOPIC_LEN = 65535; private static final int MAX_TOPIC_LEN = 65535;
private static final char NUL = '\u0000'; private static final char NUL = '\u0000';
/** /**
* Validate the topic name or topic filter * Validate the topic name or topic filter
* *
* @param topicString * @param topicString
* topic name or filter * topic name or filter
* @param wildcardAllowed * @param wildcardAllowed
* true if validate topic filter, false otherwise * true if validate topic filter, false otherwise
* @param sharedSubAllowed * @param sharedSubAllowed
* true if shared subscription is allowed, false otherwise * true if shared subscription is allowed, false otherwise
* @throws IllegalArgumentException * @throws IllegalArgumentException
* if the topic is invalid * if the topic is invalid
*/ */
public static void validate(String topicString, boolean wildcardAllowed, boolean sharedSubAllowed) public static void validate(String topicString, boolean wildcardAllowed, boolean sharedSubAllowed)
throws IllegalArgumentException { throws IllegalArgumentException {
int topicLen = 0; int topicLen = 0;
try { try {
topicLen = topicString.getBytes("UTF-8").length; topicLen = topicString.getBytes("UTF-8").length;
} catch (UnsupportedEncodingException e) { } catch (UnsupportedEncodingException e) {
throw new IllegalStateException(e.getMessage()); throw new IllegalStateException(e.getMessage());
} }
// Spec: length check // Spec: length check
// - All Topic Names and Topic Filters MUST be at least one character // - All Topic Names and Topic Filters MUST be at least one character
// long // long
// - Topic Names and Topic Filters are UTF-8 encoded strings, they MUST // - Topic Names and Topic Filters are UTF-8 encoded strings, they MUST
// NOT encode to more than 65535 bytes // NOT encode to more than 65535 bytes
if (topicLen < MIN_TOPIC_LEN || topicLen > MAX_TOPIC_LEN) { if (topicLen < MIN_TOPIC_LEN || topicLen > MAX_TOPIC_LEN) {
throw new IllegalArgumentException(String.format("Invalid topic length, should be in range[%d, %d]!", throw new IllegalArgumentException(String.format("Invalid topic length, should be in range[%d, %d]!",
new Object[] { Integer.valueOf(MIN_TOPIC_LEN), Integer.valueOf(MAX_TOPIC_LEN) })); new Object[] { Integer.valueOf(MIN_TOPIC_LEN), Integer.valueOf(MAX_TOPIC_LEN) }));
} }
// ******************************************************************************* // *******************************************************************************
// 1) This is a topic filter string that can contain wildcard characters // 1) This is a topic filter string that can contain wildcard characters
// ******************************************************************************* // *******************************************************************************
if (wildcardAllowed) { if (wildcardAllowed) {
// Only # or + // Only # or +
if (Strings.equalsAny(topicString, new String[] { MULTI_LEVEL_WILDCARD, SINGLE_LEVEL_WILDCARD })) { if (Strings.equalsAny(topicString, new String[] { MULTI_LEVEL_WILDCARD, SINGLE_LEVEL_WILDCARD })) {
return; return;
} }
// 1) Check multi-level wildcard // 1) Check multi-level wildcard
// Rule: // Rule:
// The multi-level wildcard can be specified only on its own or next // The multi-level wildcard can be specified only on its own or next
// to the topic level separator character. // to the topic level separator character.
// - Can only contains one multi-level wildcard character // - Can only contains one multi-level wildcard character
// - The multi-level wildcard must be the last character used within // - The multi-level wildcard must be the last character used within
// the topic tree // the topic tree
if (Strings.countMatches(topicString, MULTI_LEVEL_WILDCARD) > 1 if (Strings.countMatches(topicString, MULTI_LEVEL_WILDCARD) > 1
|| (topicString.contains(MULTI_LEVEL_WILDCARD) || (topicString.contains(MULTI_LEVEL_WILDCARD) && !topicString.endsWith(MULTI_LEVEL_WILDCARD_PATTERN))) {
&& !topicString.endsWith(MULTI_LEVEL_WILDCARD_PATTERN))) { throw new IllegalArgumentException("Invalid usage of multi-level wildcard in topic string: " + topicString);
throw new IllegalArgumentException( }
"Invalid usage of multi-level wildcard in topic string: " + topicString);
}
// 2) Check single-level wildcard // 2) Check single-level wildcard
// Rule: // Rule:
// The single-level wildcard can be used at any level in the topic // The single-level wildcard can be used at any level in the topic
// tree, and in conjunction with the // tree, and in conjunction with the
// multilevel wildcard. It must be used next to the topic level // multilevel wildcard. It must be used next to the topic level
// separator, except when it is specified on // separator, except when it is specified on
// its own. // its own.
validateSingleLevelWildcard(topicString); validateSingleLevelWildcard(topicString);
return; return;
} }
// Validate Shared Subscriptions // Validate Shared Subscriptions
if (!sharedSubAllowed && topicString.startsWith("$share/")) { if (!sharedSubAllowed && topicString.startsWith("$share/")) {
throw new IllegalArgumentException("Shared Subscriptions are not allowed."); throw new IllegalArgumentException("Shared Subscriptions are not allowed.");
} }
// ******************************************************************************* // *******************************************************************************
// 2) This is a topic name string that MUST NOT contains any wildcard characters // 2) This is a topic name string that MUST NOT contains any wildcard characters
// ******************************************************************************* // *******************************************************************************
if (Strings.containsAny(topicString, TOPIC_WILDCARDS)) { if (Strings.containsAny(topicString, TOPIC_WILDCARDS)) {
throw new IllegalArgumentException("The topic name MUST NOT contain any wildcard characters (#+)"); throw new IllegalArgumentException("The topic name MUST NOT contain any wildcard characters (#+)");
} }
} }
private static void validateSingleLevelWildcard(String topicString) { private static void validateSingleLevelWildcard(String topicString) {
char singleLevelWildcardChar = SINGLE_LEVEL_WILDCARD.charAt(0); char singleLevelWildcardChar = SINGLE_LEVEL_WILDCARD.charAt(0);
char topicLevelSeparatorChar = TOPIC_LEVEL_SEPARATOR.charAt(0); char topicLevelSeparatorChar = TOPIC_LEVEL_SEPARATOR.charAt(0);
char[] chars = topicString.toCharArray(); char[] chars = topicString.toCharArray();
int length = chars.length; int length = chars.length;
char prev = NUL, next = NUL; char prev = NUL, next = NUL;
for (int i = 0; i < length; i++) { for (int i = 0; i < length; i++) {
prev = (i - 1 >= 0) ? chars[i - 1] : NUL; prev = (i - 1 >= 0) ? chars[i - 1] : NUL;
next = (i + 1 < length) ? chars[i + 1] : NUL; next = (i + 1 < length) ? chars[i + 1] : NUL;
if (chars[i] == singleLevelWildcardChar) { if (chars[i] == singleLevelWildcardChar) {
// prev and next can be only '/' or none // prev and next can be only '/' or none
if (prev != topicLevelSeparatorChar && prev != NUL || next != topicLevelSeparatorChar && next != NUL) { if (prev != topicLevelSeparatorChar && prev != NUL || next != topicLevelSeparatorChar && next != NUL) {
throw new IllegalArgumentException( throw new IllegalArgumentException(String
String.format("Invalid usage of single-level wildcard in topic string '%s'!", .format("Invalid usage of single-level wildcard in topic string '%s'!", new Object[] { topicString }));
new Object[] { topicString }));
} }
} }
} }
} }
/** /**
* Check the supplied topic name and filter match * Check the supplied topic name and filter match
* *
* @param topicFilter * @param topicFilter
* topic filter: wildcards allowed * topic filter: wildcards allowed
* @param topicName * @param topicName
* topic name: wildcards not allowed * topic name: wildcards not allowed
* @return true if the topic matches the filter * @return true if the topic matches the filter
* @throws IllegalArgumentException * @throws IllegalArgumentException
* if the topic name or filter is invalid * if the topic name or filter is invalid
*/ */
public static boolean isMatched(String topicFilter, String topicName) throws IllegalArgumentException { public static boolean isMatched(String topicFilter, String topicName) throws IllegalArgumentException {
int topicPos = 0; int topicPos = 0;
int filterPos = 0; int filterPos = 0;
int topicLen = topicName.length(); int topicLen = topicName.length();
int filterLen = topicFilter.length(); int filterLen = topicFilter.length();
MqttTopicValidator.validate(topicFilter, true, true); MqttTopicValidator.validate(topicFilter, true, true);
MqttTopicValidator.validate(topicName, false, true); MqttTopicValidator.validate(topicName, false, true);
if (topicFilter.equals(topicName)) { if (topicFilter.equals(topicName)) {
return true; return true;
} }
while (filterPos < filterLen && topicPos < topicLen) { while (filterPos < filterLen && topicPos < topicLen) {
if (topicFilter.charAt(filterPos) == '#') { if (topicFilter.charAt(filterPos) == '#') {
/* /*
* next 'if' will break when topicFilter = topic/# and topicName topic/A/, * next 'if' will break when topicFilter = topic/# and topicName topic/A/, but they are matched
* but they are matched */
*/ topicPos = topicLen;
topicPos = topicLen; filterPos = filterLen;
filterPos = filterLen; break;
break; }
} if (topicName.charAt(topicPos) == '/' && topicFilter.charAt(filterPos) != '/')
if (topicName.charAt(topicPos) == '/' && topicFilter.charAt(filterPos) != '/') break;
break; if (topicFilter.charAt(filterPos) != '+' && topicFilter.charAt(filterPos) != '#'
if (topicFilter.charAt(filterPos) != '+' && topicFilter.charAt(filterPos) != '#' && topicFilter.charAt(filterPos) != topicName.charAt(topicPos))
&& topicFilter.charAt(filterPos) != topicName.charAt(topicPos)) break;
break; if (topicFilter.charAt(filterPos) == '+') { // skip until we meet the next separator, or end of string
if (topicFilter.charAt(filterPos) == '+') { // skip until we meet the next separator, or end of string int nextpos = topicPos + 1;
int nextpos = topicPos + 1; while (nextpos < topicLen && topicName.charAt(nextpos) != '/')
while (nextpos < topicLen && topicName.charAt(nextpos) != '/') nextpos = ++topicPos + 1;
nextpos = ++topicPos + 1; } else if (topicFilter.charAt(filterPos) == '#')
} else if (topicFilter.charAt(filterPos) == '#') topicPos = topicLen - 1; // skip until end of string
topicPos = topicLen - 1; // skip until end of string filterPos++;
filterPos++; topicPos++;
topicPos++; }
}
if ((topicPos == topicLen) && (filterPos == filterLen)) { if ((topicPos == topicLen) && (filterPos == filterLen)) {
return true; return true;
} else { } else {
/* /*
* https://github.com/eclipse/paho.mqtt.java/issues/418 Covers edge case to * https://github.com/eclipse/paho.mqtt.java/issues/418 Covers edge case to match sport/# to sport
* match sport/# to sport */
*/ if ((topicFilter.length() - filterPos > 0) && (topicPos == topicLen)) {
if ((topicFilter.length() - filterPos > 0) && (topicPos == topicLen)) { if (topicName.charAt(topicPos - 1) == '/' && topicFilter.charAt(filterPos) == '#')
if (topicName.charAt(topicPos - 1) == '/' && topicFilter.charAt(filterPos) == '#') return true;
return true; if (topicFilter.length() - filterPos > 1 && topicFilter.substring(filterPos, filterPos + 2).equals("/#")) {
if (topicFilter.length() - filterPos > 1 if ((topicFilter.length() - topicName.length()) == 2
&& topicFilter.substring(filterPos, filterPos + 2).equals("/#")) { && topicFilter.substring(topicFilter.length() - 2, topicFilter.length()).equals("/#")) {
if ((topicFilter.length() - topicName.length()) == 2 return true;
&& topicFilter.substring(topicFilter.length() - 2, topicFilter.length()).equals("/#")) { }
return true; }
} }
} /*
} * https://github.com/eclipse/paho.mqtt.java/issues/918
} * covers cases that include more then one wildcard
return false; * sport/+/tennis/#
} */
String[] topicFilterParts = topicFilter.split(TOPIC_LEVEL_SEPARATOR);
String[] topicParts = topicName.split(TOPIC_LEVEL_SEPARATOR);
if(topicFilterParts.length -1 == topicParts.length &&
topicFilterParts[topicFilterParts.length-1].equals( MULTI_LEVEL_WILDCARD)) {
for (int i = 0; i<topicParts.length;i++) {
if(!topicParts[i].equals(topicFilterParts[i]) && !topicFilterParts[i].equals(SINGLE_LEVEL_WILDCARD)) {
return false;
}
}
return true;
}
}
return false;
}
} }