Skip to content

Commit 5fceff8

Browse files
committed
add name length validator
1 parent 3a19914 commit 5fceff8

3 files changed

Lines changed: 216 additions & 2 deletions

File tree

sdk/src/main/java/software/amazon/lambda/durable/DurableContext.java

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121
import software.amazon.lambda.durable.validation.ParameterValidator;
2222

2323
public class DurableContext extends BaseContext {
24+
private static final String WAIT_FOR_CALLBACK_CALLBACK_SUFFIX = "-callback";
25+
private static final String WAIT_FOR_CALLBACK_SUBMITTER_SUFFIX = "-submitter";
26+
private static final int MAX_WAIT_FOR_CALLBACK_NAME_LENGTH = ParameterValidator.MAX_OPERATION_NAME_LENGTH
27+
- Math.max(WAIT_FOR_CALLBACK_CALLBACK_SUFFIX.length(), WAIT_FOR_CALLBACK_SUBMITTER_SUFFIX.length());
2428
private final AtomicInteger operationCounter;
2529
private volatile DurableLogger logger;
2630

@@ -110,6 +114,8 @@ public <T> DurableFuture<T> stepAsync(
110114
String name, TypeToken<T> typeToken, Function<StepContext, T> func, StepConfig config) {
111115
Objects.requireNonNull(config, "config cannot be null");
112116
Objects.requireNonNull(typeToken, "typeToken cannot be null");
117+
ParameterValidator.validateOperationName(name);
118+
113119
if (config.serDes() == null) {
114120
config = config.toBuilder().serDes(getDurableConfig().getSerDes()).build();
115121
}
@@ -166,6 +172,8 @@ public Void wait(Duration duration) {
166172

167173
public Void wait(String waitName, Duration duration) {
168174
ParameterValidator.validateDuration(duration, "Wait duration");
175+
ParameterValidator.validateOperationName(waitName);
176+
169177
var operationId = nextOperationId();
170178

171179
// Create and start wait operation
@@ -229,6 +237,8 @@ public <T, U> DurableFuture<T> invokeAsync(
229237
String name, String functionName, U payload, TypeToken<T> typeToken, InvokeConfig config) {
230238
Objects.requireNonNull(config, "config cannot be null");
231239
Objects.requireNonNull(typeToken, "typeToken cannot be null");
240+
ParameterValidator.validateOperationName(name);
241+
232242
if (config.serDes() == null) {
233243
config = config.toBuilder().serDes(getDurableConfig().getSerDes()).build();
234244
}
@@ -261,6 +271,7 @@ public <T> DurableCallbackFuture<T> createCallback(String name, Class<T> resultT
261271
}
262272

263273
public <T> DurableCallbackFuture<T> createCallback(String name, TypeToken<T> typeToken, CallbackConfig config) {
274+
ParameterValidator.validateOperationName(name);
264275
if (config.serDes() == null) {
265276
config = config.toBuilder().serDes(getDurableConfig().getSerDes()).build();
266277
}
@@ -295,6 +306,7 @@ public <T> DurableFuture<T> runInChildContextAsync(
295306
private <T> DurableFuture<T> runInChildContextAsync(
296307
String name, TypeToken<T> typeToken, Function<DurableContext, T> func, OperationSubType subType) {
297308
Objects.requireNonNull(typeToken, "typeToken cannot be null");
309+
ParameterValidator.validateOperationName(name);
298310
var operationId = nextOperationId();
299311

300312
var operation = new ChildContextOperation<>(
@@ -368,6 +380,9 @@ public <T> DurableFuture<T> waitForCallbackAsync(
368380
WaitForCallbackConfig waitForCallbackConfig) {
369381
Objects.requireNonNull(typeToken, "typeToken cannot be null");
370382
Objects.requireNonNull(waitForCallbackConfig, "waitForCallbackConfig cannot be null");
383+
// waitForCallback adds a suffix for the callback operation name and the submitter operation name so
384+
// the length restriction of waitForCallback name is different from the other operations.
385+
ParameterValidator.validateOperationName(name, MAX_WAIT_FOR_CALLBACK_NAME_LENGTH);
371386

372387
var finalWaitForCallbackConfig = waitForCallbackConfig.stepConfig().serDes() == null
373388
? waitForCallbackConfig.toBuilder()
@@ -382,9 +397,11 @@ public <T> DurableFuture<T> waitForCallbackAsync(
382397
typeToken,
383398
childCtx -> {
384399
var callback = childCtx.createCallback(
385-
name + "-callback", typeToken, finalWaitForCallbackConfig.callbackConfig());
400+
name + WAIT_FOR_CALLBACK_CALLBACK_SUFFIX,
401+
typeToken,
402+
finalWaitForCallbackConfig.callbackConfig());
386403
childCtx.step(
387-
name + "-submitter",
404+
name + WAIT_FOR_CALLBACK_SUBMITTER_SUFFIX,
388405
Void.class,
389406
stepCtx -> {
390407
func.accept(callback.callbackId(), stepCtx);

sdk/src/main/java/software/amazon/lambda/durable/validation/ParameterValidator.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
public final class ParameterValidator {
1313

1414
private static final long MIN_DURATION_SECONDS = 1;
15+
public static final int MAX_OPERATION_NAME_LENGTH = 256;
1516

1617
private ParameterValidator() {
1718
// Utility class - prevent instantiation
@@ -76,4 +77,30 @@ public static void validateOptionalPositiveInteger(Integer value, String paramet
7677
throw new IllegalArgumentException(parameterName + " must be positive, got: " + value);
7778
}
7879
}
80+
81+
public static void validateOperationName(String name) {
82+
validateOperationName(name, MAX_OPERATION_NAME_LENGTH);
83+
}
84+
85+
public static void validateOperationName(String name, int maxLength) {
86+
if (name == null) {
87+
// operation name is optional
88+
return;
89+
}
90+
if (name.isEmpty()) {
91+
throw new IllegalArgumentException("Operation name cannot be empty");
92+
}
93+
if (name.length() > maxLength) {
94+
throw new IllegalArgumentException(
95+
"Operation name must be less than " + maxLength + " characters, got: " + name);
96+
}
97+
98+
// validate each character is printable ASCII
99+
for (char c : name.toCharArray()) {
100+
if (c < 0x20 || c > 0x7e) {
101+
throw new IllegalArgumentException(
102+
"Operation name must contain only printable ASCII characters, got: " + name);
103+
}
104+
}
105+
}
79106
}

sdk/src/test/java/software/amazon/lambda/durable/validation/ParameterValidatorTest.java

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,4 +120,174 @@ void validateOptionalPositiveInteger_withInvalidValue_shouldThrow() {
120120

121121
assertEquals("testParam must be positive, got: 0", exception.getMessage());
122122
}
123+
124+
@Test
125+
void validateOperationName_withNull_shouldPass() {
126+
assertDoesNotThrow(() -> ParameterValidator.validateOperationName(null));
127+
}
128+
129+
@Test
130+
void validateOperationName_withValidName_shouldPass() {
131+
assertDoesNotThrow(() -> ParameterValidator.validateOperationName("test"));
132+
assertDoesNotThrow(() -> ParameterValidator.validateOperationName("my-operation"));
133+
assertDoesNotThrow(() -> ParameterValidator.validateOperationName("operation_123"));
134+
}
135+
136+
@Test
137+
void validateOperationName_withEmptyString_shouldThrow() {
138+
var exception =
139+
assertThrows(IllegalArgumentException.class, () -> ParameterValidator.validateOperationName(""));
140+
141+
assertEquals("Operation name cannot be empty", exception.getMessage());
142+
}
143+
144+
@Test
145+
void validateOperationName_withMaxLength_shouldPass() {
146+
var name = "a".repeat(ParameterValidator.MAX_OPERATION_NAME_LENGTH);
147+
assertDoesNotThrow(() -> ParameterValidator.validateOperationName(name));
148+
}
149+
150+
@Test
151+
void validateOperationName_exceedingMaxLength_shouldThrow() {
152+
var name = "a".repeat(ParameterValidator.MAX_OPERATION_NAME_LENGTH + 1);
153+
var exception =
154+
assertThrows(IllegalArgumentException.class, () -> ParameterValidator.validateOperationName(name));
155+
156+
assertEquals(
157+
"Operation name must be less than " + ParameterValidator.MAX_OPERATION_NAME_LENGTH
158+
+ " characters, got: " + name,
159+
exception.getMessage());
160+
}
161+
162+
@Test
163+
void validateOperationName_withCustomMaxLength_withNull_shouldPass() {
164+
assertDoesNotThrow(() -> ParameterValidator.validateOperationName(null, 100));
165+
}
166+
167+
@Test
168+
void validateOperationName_withCustomMaxLength_withValidName_shouldPass() {
169+
assertDoesNotThrow(() -> ParameterValidator.validateOperationName("test", 100));
170+
assertDoesNotThrow(() -> ParameterValidator.validateOperationName("a".repeat(100), 100));
171+
}
172+
173+
@Test
174+
void validateOperationName_withCustomMaxLength_withEmptyString_shouldThrow() {
175+
var exception =
176+
assertThrows(IllegalArgumentException.class, () -> ParameterValidator.validateOperationName("", 100));
177+
178+
assertEquals("Operation name cannot be empty", exception.getMessage());
179+
}
180+
181+
@Test
182+
void validateOperationName_withCustomMaxLength_exceedingLimit_shouldThrow() {
183+
var customMaxLength = 50;
184+
var name = "a".repeat(customMaxLength + 1);
185+
var exception = assertThrows(
186+
IllegalArgumentException.class, () -> ParameterValidator.validateOperationName(name, customMaxLength));
187+
188+
assertEquals(
189+
"Operation name must be less than " + customMaxLength + " characters, got: " + name,
190+
exception.getMessage());
191+
}
192+
193+
@Test
194+
void validateOperationName_withCustomMaxLength_atExactLimit_shouldPass() {
195+
var customMaxLength = 50;
196+
var name = "a".repeat(customMaxLength);
197+
assertDoesNotThrow(() -> ParameterValidator.validateOperationName(name, customMaxLength));
198+
}
199+
200+
@Test
201+
void validateOperationName_withSpecialCharacters_shouldPass() {
202+
assertDoesNotThrow(() -> ParameterValidator.validateOperationName("operation-name"));
203+
assertDoesNotThrow(() -> ParameterValidator.validateOperationName("operation_name"));
204+
assertDoesNotThrow(() -> ParameterValidator.validateOperationName("operation.name"));
205+
assertDoesNotThrow(() -> ParameterValidator.validateOperationName("operation:name"));
206+
assertDoesNotThrow(() -> ParameterValidator.validateOperationName("operation/name"));
207+
assertDoesNotThrow(() -> ParameterValidator.validateOperationName("operation@name"));
208+
assertDoesNotThrow(() -> ParameterValidator.validateOperationName("operation#name"));
209+
assertDoesNotThrow(() -> ParameterValidator.validateOperationName("operation$name"));
210+
}
211+
212+
@Test
213+
void validateOperationName_withUnicodeCharacters_shouldThrow() {
214+
var exception1 =
215+
assertThrows(IllegalArgumentException.class, () -> ParameterValidator.validateOperationName("操作名称"));
216+
assertEquals("Operation name must contain only printable ASCII characters, got: 操作名称", exception1.getMessage());
217+
218+
var exception2 = assertThrows(
219+
IllegalArgumentException.class, () -> ParameterValidator.validateOperationName("opération"));
220+
assertEquals(
221+
"Operation name must contain only printable ASCII characters, got: opération", exception2.getMessage());
222+
223+
var exception3 = assertThrows(
224+
IllegalArgumentException.class, () -> ParameterValidator.validateOperationName("операция"));
225+
assertEquals(
226+
"Operation name must contain only printable ASCII characters, got: операция", exception3.getMessage());
227+
}
228+
229+
@Test
230+
void validateOperationName_withWhitespace_shouldPass() {
231+
assertDoesNotThrow(() -> ParameterValidator.validateOperationName("operation name"));
232+
assertDoesNotThrow(() -> ParameterValidator.validateOperationName(" operation"));
233+
assertDoesNotThrow(() -> ParameterValidator.validateOperationName("operation "));
234+
}
235+
236+
@Test
237+
void validateOperationName_withControlCharacters_shouldThrow() {
238+
var exception1 = assertThrows(
239+
IllegalArgumentException.class, () -> ParameterValidator.validateOperationName("operation\nname"));
240+
assertEquals(
241+
"Operation name must contain only printable ASCII characters, got: operation\nname",
242+
exception1.getMessage());
243+
244+
var exception2 = assertThrows(
245+
IllegalArgumentException.class, () -> ParameterValidator.validateOperationName("operation\tname"));
246+
assertEquals(
247+
"Operation name must contain only printable ASCII characters, got: operation\tname",
248+
exception2.getMessage());
249+
250+
var exception3 = assertThrows(
251+
IllegalArgumentException.class, () -> ParameterValidator.validateOperationName("operation\rname"));
252+
assertEquals(
253+
"Operation name must contain only printable ASCII characters, got: operation\rname",
254+
exception3.getMessage());
255+
}
256+
257+
@Test
258+
void validateOperationName_withNonPrintableASCII_shouldThrow() {
259+
// Test character below printable range (0x1F)
260+
var exception1 = assertThrows(
261+
IllegalArgumentException.class, () -> ParameterValidator.validateOperationName("test\u001Fname"));
262+
assertEquals(
263+
"Operation name must contain only printable ASCII characters, got: test\u001Fname",
264+
exception1.getMessage());
265+
266+
// Test character above printable range (0x7F - DEL)
267+
var exception2 = assertThrows(
268+
IllegalArgumentException.class, () -> ParameterValidator.validateOperationName("test\u007Fname"));
269+
assertEquals(
270+
"Operation name must contain only printable ASCII characters, got: test\u007Fname",
271+
exception2.getMessage());
272+
}
273+
274+
@Test
275+
void validateOperationName_withPrintableASCIIBoundaries_shouldPass() {
276+
// Test lower boundary (0x20 - space)
277+
assertDoesNotThrow(() -> ParameterValidator.validateOperationName(" "));
278+
279+
// Test upper boundary (0x7E - tilde)
280+
assertDoesNotThrow(() -> ParameterValidator.validateOperationName("~"));
281+
282+
// Test all printable ASCII characters
283+
assertDoesNotThrow(() -> ParameterValidator.validateOperationName(
284+
"!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"));
285+
}
286+
287+
@Test
288+
void validateOperationName_withSingleCharacter_shouldPass() {
289+
assertDoesNotThrow(() -> ParameterValidator.validateOperationName("a"));
290+
assertDoesNotThrow(() -> ParameterValidator.validateOperationName("1"));
291+
assertDoesNotThrow(() -> ParameterValidator.validateOperationName("-"));
292+
}
123293
}

0 commit comments

Comments
 (0)