/
MqttTopicValidator.java
215 lines (187 loc) · 7.88 KB
/
MqttTopicValidator.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
package org.eclipse.paho.mqttv5.common.util;
import java.io.UnsupportedEncodingException;
public class MqttTopicValidator {
/**
* The forward slash (/) is used to separate each level within a topic tree and
* provide a hierarchical structure to the topic space. The use of the topic
* level separator is significant when the two wildcard characters are
* encountered in topics specified by subscribers.
*/
public static final String TOPIC_LEVEL_SEPARATOR = "/";
/**
* Multi-level wildcard The number sign (#) is a wildcard character that matches
* any number of levels within a topic.
*/
public static final String MULTI_LEVEL_WILDCARD = "#";
/**
* Single-level wildcard The plus sign (+) is a wildcard character that matches
* only one topic level.
*/
public static final String SINGLE_LEVEL_WILDCARD = "+";
/**
* Multi-level wildcard pattern(/#)
*/
public static final String MULTI_LEVEL_WILDCARD_PATTERN = TOPIC_LEVEL_SEPARATOR + MULTI_LEVEL_WILDCARD;
/**
* Topic wildcards (#+)
*/
public static final String TOPIC_WILDCARDS = MULTI_LEVEL_WILDCARD + SINGLE_LEVEL_WILDCARD;
// topic name and topic filter length range defined in the spec
private static final int MIN_TOPIC_LEN = 1;
private static final int MAX_TOPIC_LEN = 65535;
private static final char NUL = '\u0000';
/**
* Validate the topic name or topic filter
*
* @param topicString
* topic name or filter
* @param wildcardAllowed
* true if validate topic filter, false otherwise
* @throws IllegalArgumentException
* if the topic is invalid
*/
public static void validate(String topicString, boolean wildcardAllowed, boolean sharedSubAllowed)
throws IllegalArgumentException {
int topicLen = 0;
try {
topicLen = topicString.getBytes("UTF-8").length;
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException(e.getMessage());
}
// Spec: length check
// - All Topic Names and Topic Filters MUST be at least one character
// long
// - Topic Names and Topic Filters are UTF-8 encoded strings, they MUST
// NOT encode to more than 65535 bytes
if (topicLen < MIN_TOPIC_LEN || topicLen > MAX_TOPIC_LEN) {
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) }));
}
// *******************************************************************************
// 1) This is a topic filter string that can contain wildcard characters
// *******************************************************************************
if (wildcardAllowed) {
// Only # or +
if (Strings.equalsAny(topicString, new String[] { MULTI_LEVEL_WILDCARD, SINGLE_LEVEL_WILDCARD })) {
return;
}
// 1) Check multi-level wildcard
// Rule:
// The multi-level wildcard can be specified only on its own or next
// to the topic level separator character.
// - Can only contains one multi-level wildcard character
// - The multi-level wildcard must be the last character used within
// the topic tree
if (Strings.countMatches(topicString, MULTI_LEVEL_WILDCARD) > 1
|| (topicString.contains(MULTI_LEVEL_WILDCARD)
&& !topicString.endsWith(MULTI_LEVEL_WILDCARD_PATTERN))) {
throw new IllegalArgumentException(
"Invalid usage of multi-level wildcard in topic string: " + topicString);
}
// 2) Check single-level wildcard
// Rule:
// The single-level wildcard can be used at any level in the topic
// tree, and in conjunction with the
// multilevel wildcard. It must be used next to the topic level
// separator, except when it is specified on
// its own.
validateSingleLevelWildcard(topicString);
return;
}
// Validate Shared Subscriptions
if (!sharedSubAllowed && topicString.startsWith("$share/")) {
throw new IllegalArgumentException("Shared Subscriptions are not allowed.");
}
// *******************************************************************************
// 2) This is a topic name string that MUST NOT contains any wildcard characters
// *******************************************************************************
if (Strings.containsAny(topicString, TOPIC_WILDCARDS)) {
throw new IllegalArgumentException("The topic name MUST NOT contain any wildcard characters (#+)");
}
}
private static void validateSingleLevelWildcard(String topicString) {
char singleLevelWildcardChar = SINGLE_LEVEL_WILDCARD.charAt(0);
char topicLevelSeparatorChar = TOPIC_LEVEL_SEPARATOR.charAt(0);
char[] chars = topicString.toCharArray();
int length = chars.length;
char prev = NUL, next = NUL;
for (int i = 0; i < length; i++) {
prev = (i - 1 >= 0) ? chars[i - 1] : NUL;
next = (i + 1 < length) ? chars[i + 1] : NUL;
if (chars[i] == singleLevelWildcardChar) {
// prev and next can be only '/' or none
if (prev != topicLevelSeparatorChar && prev != NUL || next != topicLevelSeparatorChar && next != NUL) {
throw new IllegalArgumentException(
String.format("Invalid usage of single-level wildcard in topic string '%s'!",
new Object[] { topicString }));
}
}
}
}
/**
* Check the supplied topic name and filter match
*
* @param topicFilter
* topic filter: wildcards allowed
* @param topicName
* topic name: wildcards not allowed
* @return true if the topic matches the filter
* @throws IllegalArgumentException
* if the topic name or filter is invalid
*/
public static boolean isMatched(String topicFilter, String topicName) throws IllegalArgumentException {
int topicPos = 0;
int filterPos = 0;
int topicLen = topicName.length();
int filterLen = topicFilter.length();
MqttTopicValidator.validate(topicFilter, true, true);
MqttTopicValidator.validate(topicName, false, true);
if (topicFilter.equals(topicName)) {
return true;
}
while (filterPos < filterLen && topicPos < topicLen) {
if (topicFilter.charAt(filterPos) == '#') {
/*
* next 'if' will break when topicFilter = topic/# and topicName topic/A/,
* but they are matched
*/
topicPos = topicLen;
filterPos = filterLen;
break;
}
if (topicName.charAt(topicPos) == '/' && topicFilter.charAt(filterPos) != '/')
break;
if (topicFilter.charAt(filterPos) != '+' && topicFilter.charAt(filterPos) != '#'
&& topicFilter.charAt(filterPos) != topicName.charAt(topicPos))
break;
if (topicFilter.charAt(filterPos) == '+') { // skip until we meet the next separator, or end of string
int nextpos = topicPos + 1;
while (nextpos < topicLen && topicName.charAt(nextpos) != '/')
nextpos = ++topicPos + 1;
} else if (topicFilter.charAt(filterPos) == '#')
topicPos = topicLen - 1; // skip until end of string
filterPos++;
topicPos++;
}
if ((topicPos == topicLen) && (filterPos == filterLen)) {
return true;
} else {
/*
* https://github.com/eclipse/paho.mqtt.java/issues/418 Covers edge case to
* match sport/# to sport
*/
if ((topicFilter.length() - filterPos > 0) && (topicPos == topicLen)) {
if (topicName.charAt(topicPos - 1) == '/' && topicFilter.charAt(filterPos) == '#')
return true;
if (topicFilter.length() - filterPos > 1
&& topicFilter.substring(filterPos, filterPos + 2).equals("/#")) {
if ((topicFilter.length() - topicName.length()) == 2
&& topicFilter.substring(topicFilter.length() - 2, topicFilter.length()).equals("/#")) {
return true;
}
}
}
}
return false;
}
}