From 6e141f2028486f8c5b5d428a2c1ca430ff0a50a0 Mon Sep 17 00:00:00 2001 From: Prayush Jain Date: Sat, 26 Sep 2026 15:26:50 +0530 Subject: [PATCH] Validate BotMaker collection helpers before index access. Empty or out-of-range inputs to First/Last/FirstN/Slice/ElementAtIndex threw raw IndexOutOfBoundsException, which aborted scarecrow labeling rules. Surface typed FunctionFailure with clear messages instead. Fixes #207 Related to #58 Co-authored-by: Cursor --- .../function/collection/ElementAtIndex.java | 26 ++++++++++++++- .../botmaker/function/collection/First.java | 7 ++++ .../botmaker/function/collection/FirstN.java | 11 ++++++- .../botmaker/function/collection/Last.java | 7 ++++ .../botmaker/function/collection/Slice.java | 33 +++++++++++++++++++ 5 files changed, 82 insertions(+), 2 deletions(-) diff --git a/botmaker/src/java/com/twitter/botmaker/function/collection/ElementAtIndex.java b/botmaker/src/java/com/twitter/botmaker/function/collection/ElementAtIndex.java index 450d3768c..8c0d12c2c 100644 --- a/botmaker/src/java/com/twitter/botmaker/function/collection/ElementAtIndex.java +++ b/botmaker/src/java/com/twitter/botmaker/function/collection/ElementAtIndex.java @@ -8,6 +8,7 @@ import com.twitter.botmaker.Context; import com.twitter.botmaker.compiler.ActionLevel; import com.twitter.botmaker.compiler.BotMakerFunction; +import com.twitter.botmaker.compiler.exceptions.FunctionFailure; import com.twitter.botmaker.compiler.exceptions.SemanticCheckFailure; import com.twitter.botmaker.compiler.types.Type; import com.twitter.botmaker.function.FunctionNode2; @@ -55,7 +56,30 @@ protected CacheLevel getCacheLevel() { @Override protected Object apply(Context context, List list, Long index) { - return list.get(index.intValue()); + if (list == null) { + throw new FunctionFailure( + this, + context.getStackFrames(), + new IllegalArgumentException("ElementAtIndex() called on null list")); + } + if (index == null) { + throw new FunctionFailure( + this, + context.getStackFrames(), + new IllegalArgumentException("ElementAtIndex() requires a non-null index")); + } + int i = index.intValue(); + if (i < 0 || i >= list.size()) { + throw new FunctionFailure( + this, + context.getStackFrames(), + new IllegalArgumentException( + String.format( + "ElementAtIndex() index out of range: index=%d size=%d", + i, + list.size()))); + } + return list.get(i); } } diff --git a/botmaker/src/java/com/twitter/botmaker/function/collection/First.java b/botmaker/src/java/com/twitter/botmaker/function/collection/First.java index 5831dd25a..56c58be73 100644 --- a/botmaker/src/java/com/twitter/botmaker/function/collection/First.java +++ b/botmaker/src/java/com/twitter/botmaker/function/collection/First.java @@ -8,6 +8,7 @@ import com.twitter.botmaker.Context; import com.twitter.botmaker.compiler.ActionLevel; import com.twitter.botmaker.compiler.BotMakerFunction; +import com.twitter.botmaker.compiler.exceptions.FunctionFailure; import com.twitter.botmaker.compiler.exceptions.SemanticCheckFailure; import com.twitter.botmaker.compiler.types.Type; import com.twitter.botmaker.function.FunctionNode1; @@ -46,6 +47,12 @@ protected CacheLevel getCacheLevel() { @Override public Object apply(Context context, List list) { + if (list == null || list.isEmpty()) { + throw new FunctionFailure( + this, + context.getStackFrames(), + new IllegalArgumentException("First() called on empty list")); + } return list.get(0); } } diff --git a/botmaker/src/java/com/twitter/botmaker/function/collection/FirstN.java b/botmaker/src/java/com/twitter/botmaker/function/collection/FirstN.java index a06e9d140..be04ced7f 100644 --- a/botmaker/src/java/com/twitter/botmaker/function/collection/FirstN.java +++ b/botmaker/src/java/com/twitter/botmaker/function/collection/FirstN.java @@ -8,6 +8,7 @@ import com.twitter.botmaker.Context; import com.twitter.botmaker.compiler.ActionLevel; import com.twitter.botmaker.compiler.BotMakerFunction; +import com.twitter.botmaker.compiler.exceptions.FunctionFailure; import com.twitter.botmaker.compiler.exceptions.SemanticCheckFailure; import com.twitter.botmaker.compiler.types.Type; import com.twitter.botmaker.function.FunctionNode2; @@ -54,6 +55,14 @@ protected CacheLevel getCacheLevel() { @Override public Object apply(Context context, List list, Long n) { - return list.subList(0, Math.min(n.intValue(), list.size())); + if (list == null) { + throw new FunctionFailure( + this, + context.getStackFrames(), + new IllegalArgumentException("FirstN() called on null list")); + } + // Clamp negative n so subList(0, negative) cannot throw IndexOutOfBoundsException. + long count = Math.max(0L, n == null ? 0L : n.longValue()); + return list.subList(0, (int) Math.min(count, list.size())); } } diff --git a/botmaker/src/java/com/twitter/botmaker/function/collection/Last.java b/botmaker/src/java/com/twitter/botmaker/function/collection/Last.java index 3d979c712..21417ea84 100644 --- a/botmaker/src/java/com/twitter/botmaker/function/collection/Last.java +++ b/botmaker/src/java/com/twitter/botmaker/function/collection/Last.java @@ -8,6 +8,7 @@ import com.twitter.botmaker.Context; import com.twitter.botmaker.compiler.ActionLevel; import com.twitter.botmaker.compiler.BotMakerFunction; +import com.twitter.botmaker.compiler.exceptions.FunctionFailure; import com.twitter.botmaker.compiler.exceptions.SemanticCheckFailure; import com.twitter.botmaker.compiler.types.Type; import com.twitter.botmaker.function.FunctionNode1; @@ -46,6 +47,12 @@ protected CacheLevel getCacheLevel() { @Override public Object apply(Context context, List list) { + if (list == null || list.isEmpty()) { + throw new FunctionFailure( + this, + context.getStackFrames(), + new IllegalArgumentException("Last() called on empty list")); + } return list.get(list.size() - 1); } } diff --git a/botmaker/src/java/com/twitter/botmaker/function/collection/Slice.java b/botmaker/src/java/com/twitter/botmaker/function/collection/Slice.java index 1d6965aa5..7061f2e1d 100644 --- a/botmaker/src/java/com/twitter/botmaker/function/collection/Slice.java +++ b/botmaker/src/java/com/twitter/botmaker/function/collection/Slice.java @@ -10,6 +10,7 @@ import com.twitter.botmaker.Context; import com.twitter.botmaker.compiler.ActionLevel; import com.twitter.botmaker.compiler.BotMakerFunction; +import com.twitter.botmaker.compiler.exceptions.FunctionFailure; import com.twitter.botmaker.compiler.exceptions.SemanticCheckFailure; import com.twitter.botmaker.compiler.types.Type; import com.twitter.botmaker.function.FunctionNode2O1; @@ -73,6 +74,7 @@ private static ImmutableList validateType(ImmutableList childr @Override protected Object apply( Context context, Object input, Long beginIndex, Long endIndex) { + validateSliceBounds(context, input, beginIndex, endIndex); if (input instanceof String) { return ((String) input).substring(beginIndex.intValue(), endIndex.intValue()); } else if (input instanceof List) { @@ -96,6 +98,37 @@ protected Object apply(Context context, Object input, Long beginIndex) } } + private void validateSliceBounds( + Context context, Object input, Long beginIndex, Long endIndex) { + if (beginIndex == null || endIndex == null) { + throw new FunctionFailure( + this, + context.getStackFrames(), + new IllegalArgumentException("Slice() requires non-null begin and end indices")); + } + int begin = beginIndex.intValue(); + int end = endIndex.intValue(); + final int max; + if (input instanceof String) { + max = ((String) input).length(); + } else if (input instanceof List) { + max = ((List) input).size(); + } else { + throw new IllegalArgumentException(mkErrorMessage(input.getClass())); + } + if (begin < 0 || end < 0 || begin > end || end > max) { + throw new FunctionFailure( + this, + context.getStackFrames(), + new IllegalArgumentException( + String.format( + "Slice() indices out of range: begin=%d end=%d length=%d", + begin, + end, + max))); + } + } + private static String mkErrorMessage(Class input) { return String.format( "expecting the input to be of String or List type but received: %s for %s",