@@ -76,6 +76,55 @@ pub(crate) fn opt_str<'a>(body: &'a Value, field: &str) -> Option<&'a str> {
7676 body. get ( field) . and_then ( |v| v. as_str ( ) )
7777}
7878
79+ /// Paginate a list of ARN-like strings with a key-based cursor instead of a
80+ /// numeric offset. The list is sorted for a stable order (HashMap/BTreeMap
81+ /// value order is not a contract callers can rely on), and the returned
82+ /// `nextToken` is an opaque base64 of the last-emitted key. Resuming finds the
83+ /// first key strictly greater than the cursor, so pages stay correct even when
84+ /// items are added or removed between calls — a numeric offset would skip or
85+ /// duplicate rows there. A non-empty token that doesn't base64-decode (a stale
86+ /// or corrupted cursor we never emitted) is treated as past the end and yields
87+ /// an empty page — restarting at the beginning would silently re-list page 1
88+ /// and risk an infinite pagination loop for the client.
89+ pub ( crate ) fn paginate_arns (
90+ mut arns : Vec < String > ,
91+ next_token : & str ,
92+ max_results : usize ,
93+ ) -> ( Vec < String > , Option < String > ) {
94+ use base64:: Engine ;
95+ arns. sort ( ) ;
96+ arns. dedup ( ) ;
97+ let cursor = if next_token. is_empty ( ) {
98+ None
99+ } else {
100+ base64:: engine:: general_purpose:: STANDARD
101+ . decode ( next_token)
102+ . ok ( )
103+ . and_then ( |b| String :: from_utf8 ( b) . ok ( ) )
104+ } ;
105+ let start = if next_token. is_empty ( ) {
106+ 0
107+ } else {
108+ match & cursor {
109+ Some ( c) => arns
110+ . iter ( )
111+ . position ( |a| a. as_str ( ) > c. as_str ( ) )
112+ . unwrap_or ( arns. len ( ) ) ,
113+ // Non-empty but undecodable token: a cursor we never emitted.
114+ // Treat as past the end (empty page) rather than restarting at 0.
115+ None => arns. len ( ) ,
116+ }
117+ } ;
118+ let end = ( start + max_results) . min ( arns. len ( ) ) ;
119+ let page = arns[ start..end] . to_vec ( ) ;
120+ let next = if end < arns. len ( ) {
121+ Some ( base64:: engine:: general_purpose:: STANDARD . encode ( arns[ end - 1 ] . as_bytes ( ) ) )
122+ } else {
123+ None
124+ } ;
125+ ( page, next)
126+ }
127+
79128/// Validate that an optional string field, if present, is one of the allowed
80129/// enum values. Returns InvalidParameterException if the field is set to a
81130/// value outside `allowed`. Absent or null fields are accepted.
@@ -1508,3 +1557,43 @@ pub(crate) fn task_protection_json(task: &Task) -> Value {
15081557 "expirationDate" : p. and_then( |p| p. expiration) . map( |e| e. timestamp( ) ) ,
15091558 } )
15101559}
1560+
1561+ #[ cfg( test) ]
1562+ mod pagination_tests {
1563+ use super :: paginate_arns;
1564+
1565+ #[ test]
1566+ fn paginate_arns_key_cursor_is_stable_across_delete ( ) {
1567+ let all: Vec < String > = ( 0 ..5 ) . map ( |i| format ! ( "arn:{i}" ) ) . collect ( ) ;
1568+ let ( p1, next) = paginate_arns ( all. clone ( ) , "" , 2 ) ;
1569+ assert_eq ! ( p1, vec![ "arn:0" , "arn:1" ] ) ;
1570+ let token = next. expect ( "nextToken after page 1" ) ;
1571+
1572+ // Delete the first item before fetching page 2. A numeric offset (2)
1573+ // would skip "arn:2"; the key cursor resumes correctly after "arn:1".
1574+ let after: Vec < String > = all. iter ( ) . filter ( |a| * a != "arn:0" ) . cloned ( ) . collect ( ) ;
1575+ let ( p2, next2) = paginate_arns ( after, & token, 2 ) ;
1576+ assert_eq ! ( p2, vec![ "arn:2" , "arn:3" ] ) ;
1577+ assert ! ( next2. is_some( ) ) ;
1578+ }
1579+
1580+ #[ test]
1581+ fn paginate_arns_sorts_and_dedups_and_tolerates_garbage_token ( ) {
1582+ let dupes = vec ! [
1583+ "b" . to_string( ) ,
1584+ "a" . to_string( ) ,
1585+ "a" . to_string( ) ,
1586+ "c" . to_string( ) ,
1587+ ] ;
1588+ let ( page, _) = paginate_arns ( dupes, "" , 10 ) ;
1589+ assert_eq ! ( page, vec![ "a" , "b" , "c" ] ) ;
1590+ // A non-empty, undecodable token is treated as past the end: empty
1591+ // page + no nextToken, never a silent restart at page 1.
1592+ let ( page, next) = paginate_arns ( vec ! [ "a" . into( ) , "b" . into( ) ] , "!!!not-base64" , 1 ) ;
1593+ assert ! ( page. is_empty( ) ) ;
1594+ assert ! ( next. is_none( ) ) ;
1595+ // An empty token still starts at the beginning.
1596+ let ( page, _) = paginate_arns ( vec ! [ "a" . into( ) , "b" . into( ) ] , "" , 1 ) ;
1597+ assert_eq ! ( page, vec![ "a" ] ) ;
1598+ }
1599+ }
0 commit comments