Skip to content

Commit f6ad36e

Browse files
committed
Fix DurationParser allowing invalid input by not using regex and manually parsing the input
1 parent 3c83f9a commit f6ad36e

2 files changed

Lines changed: 92 additions & 19 deletions

File tree

cloud-core/src/main/java/org/incendo/cloud/parser/standard/DurationParser.java

Lines changed: 56 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,12 @@
2525

2626
import java.time.Duration;
2727
import java.util.Collections;
28-
import java.util.regex.Matcher;
29-
import java.util.regex.Pattern;
3028
import java.util.stream.Collectors;
3129
import java.util.stream.IntStream;
3230
import java.util.stream.Stream;
3331
import org.apiguardian.api.API;
3432
import org.checkerframework.checker.nullness.qual.NonNull;
33+
import org.checkerframework.checker.nullness.qual.Nullable;
3534
import org.incendo.cloud.caption.CaptionVariable;
3635
import org.incendo.cloud.caption.StandardCaptionKeys;
3736
import org.incendo.cloud.component.CommandComponent;
@@ -73,48 +72,65 @@ public final class DurationParser<C> implements ArgumentParser<C, Duration>, Blo
7372
return CommandComponent.<C, Duration>builder().parser(durationParser());
7473
}
7574

76-
/**
77-
* Matches durations in the format of: <code>2d15h7m12s</code>
78-
*/
79-
private static final Pattern DURATION_PATTERN = Pattern.compile("(([1-9][0-9]+|[1-9])[dhms])");
80-
8175
@Override
8276
public @NonNull ArgumentParseResult<Duration> parse(
8377
final @NonNull CommandContext<C> commandContext,
8478
final @NonNull CommandInput commandInput
8579
) {
86-
final String input = commandInput.readString();
87-
88-
final Matcher matcher = DURATION_PATTERN.matcher(input);
80+
final String input = commandInput.peekString();
8981

9082
Duration duration = Duration.ofNanos(0);
9183

92-
while (matcher.find()) {
93-
String group = matcher.group();
94-
String timeUnit = String.valueOf(group.charAt(group.length() - 1));
95-
int timeValue = Integer.parseInt(group.substring(0, group.length() - 1));
84+
// substring range enclosing digits and unit (single char)
85+
int rangeStart = 0;
86+
int cursor = 0;
87+
88+
while (cursor < input.length()) {
89+
// advance cursor until time unit or we reach end of input (in which case it's invalid anyway)
90+
while (cursor < input.length() && Character.isDigit(input.charAt(cursor))) {
91+
cursor += 1;
92+
}
93+
94+
// reached end of input with no time unit
95+
if (cursor == input.length()) {
96+
return ArgumentParseResult.failure(new DurationParseException(input, commandContext));
97+
}
98+
99+
final long timeValue;
100+
try {
101+
timeValue = Long.parseUnsignedLong(input.substring(rangeStart, cursor));
102+
} catch (final NumberFormatException ex) {
103+
return ArgumentParseResult.failure(new DurationParseException(ex, input, commandContext));
104+
}
105+
106+
final char timeUnit = input.charAt(cursor);
96107
switch (timeUnit) {
97-
case "d":
108+
case 'd':
98109
duration = duration.plusDays(timeValue);
99110
break;
100-
case "h":
111+
case 'h':
101112
duration = duration.plusHours(timeValue);
102113
break;
103-
case "m":
114+
case 'm':
104115
duration = duration.plusMinutes(timeValue);
105116
break;
106-
case "s":
117+
case 's':
107118
duration = duration.plusSeconds(timeValue);
108119
break;
109120
default:
110121
return ArgumentParseResult.failure(new DurationParseException(input, commandContext));
111122
}
123+
124+
// skip unit, reset rangeStart to start of next segment
125+
cursor += 1;
126+
rangeStart = cursor;
112127
}
113128

114129
if (duration.isZero()) {
115130
return ArgumentParseResult.failure(new DurationParseException(input, commandContext));
116131
}
117132

133+
commandInput.readString(); // pop read input on success
118134
return ArgumentParseResult.success(duration);
119135
}
120136

@@ -172,6 +188,28 @@ public DurationParseException(
172188
this.input = input;
173189
}
174190

191+
/**
192+
* Construct a new {@link DurationParseException} with a causing exception.
193+
*
194+
* @param cause cause of exception
195+
* @param input input string
196+
* @param context command context
197+
*/
198+
public DurationParseException(
199+
final @Nullable Throwable cause,
200+
final @NonNull String input,
201+
final @NonNull CommandContext<?> context
202+
) {
203+
super(
204+
cause,
205+
DurationParser.class,
206+
context,
207+
StandardCaptionKeys.ARGUMENT_PARSE_FAILURE_DURATION,
208+
CaptionVariable.of("input", input)
209+
);
210+
this.input = input;
211+
}
212+
175213
/**
176214
* Returns the supplied input string.
177215
*

cloud-core/src/test/java/org/incendo/cloud/parser/standard/DurationParserTest.java

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,15 +75,50 @@ void single_multiple_units() {
7575
}
7676

7777
@Test
78-
void invalid_format_failing() {
78+
void invalid_format_no_time_value() {
7979
Assertions.assertThrows(
8080
CompletionException.class,
8181
() -> manager.commandExecutor().executeCommand(new TestCommandSender(), "duration d").join()
8282
);
83+
}
8384

85+
@Test
86+
void invalid_format_no_time_unit() {
87+
Assertions.assertThrows(
88+
CompletionException.class,
89+
() -> manager.commandExecutor().executeCommand(new TestCommandSender(), "duration 1").join()
90+
);
91+
}
92+
93+
@Test
94+
void invalid_format_invalid_unit() {
8495
Assertions.assertThrows(
8596
CompletionException.class,
8697
() -> manager.commandExecutor().executeCommand(new TestCommandSender(), "duration 1x").join()
8798
);
8899
}
100+
101+
@Test
102+
void invalid_format_leading_garbage() {
103+
Assertions.assertThrows(
104+
CompletionException.class,
105+
() -> manager.commandExecutor().executeCommand(new TestCommandSender(), "duration foo1d").join()
106+
);
107+
}
108+
109+
@Test
110+
void invalid_format_garbage() {
111+
Assertions.assertThrows(
112+
CompletionException.class,
113+
() -> manager.commandExecutor().executeCommand(new TestCommandSender(), "duration 1dfoo2h").join()
114+
);
115+
}
116+
117+
@Test
118+
void invalid_format_trailing_garbage() {
119+
Assertions.assertThrows(
120+
CompletionException.class,
121+
() -> manager.commandExecutor().executeCommand(new TestCommandSender(), "duration 1dfoo").join()
122+
);
123+
}
89124
}

0 commit comments

Comments
 (0)