@@ -97,17 +97,23 @@ def _subsequence_indices(
9797 path : list [dict [str , Any ]],
9898 expected : list [str ],
9999 max_hop_span : Optional [int ] = None ,
100+ max_path_length : Optional [int ] = None ,
100101) -> Optional [tuple [int , int ]]:
101102 """Two-pointer subsequence prefix match with gaps allowed.
102103
103104 Each entry in *path* is a dict with ``position`` and ``node_hash``.
104105 *expected* is the ordered list of uppercase hash prefixes to find.
105106 A hop matches when ``node_hash.startswith(expected_hash)``.
106107 ``max_hop_span`` constrains ``position(last) - position(first)`` when set.
108+ ``max_path_length`` caps the total number of hops in *path*; when set
109+ and ``len(path)`` exceeds it, the packet is rejected up front (returns
110+ ``None``) without running the match.
107111
108112 Returns the ``(first_i, last_i)`` indices into *path* of the matched
109113 endpoints, or ``None`` when no match is found.
110114 """
115+ if max_path_length is not None and len (path ) > max_path_length :
116+ return None
111117 if not expected :
112118 return None
113119 pi = 0
@@ -144,21 +150,26 @@ def is_subsequence(
144150 path : list [dict [str , Any ]],
145151 expected : list [str ],
146152 max_hop_span : Optional [int ] = None ,
153+ max_path_length : Optional [int ] = None ,
147154) -> bool :
148155 """Pure two-pointer subsequence prefix match with gaps allowed.
149156
150157 Each entry in *path* is a dict with ``position`` and ``node_hash``.
151158 *expected* is the ordered list of uppercase hash prefixes to find.
152159 A hop matches when ``node_hash.startswith(expected_hash)``.
153160 ``max_hop_span`` constrains ``position(last) - position(first)`` when set.
161+ ``max_path_length`` caps the total number of hops in *path* when set.
154162 """
155- return _subsequence_indices (path , expected , max_hop_span ) is not None
163+ return (
164+ _subsequence_indices (path , expected , max_hop_span , max_path_length ) is not None
165+ )
156166
157167
158168def _matched_subpath (
159169 hops : list [dict [str , Any ]],
160170 expected : list [str ],
161171 max_hop_span : Optional [int ] = None ,
172+ max_path_length : Optional [int ] = None ,
162173 reversible : bool = True ,
163174) -> Optional [list [dict [str , Any ]]]:
164175 """Return the slice of *hops* between the first and last matched node.
@@ -169,7 +180,7 @@ def _matched_subpath(
169180 (never reversed), so a reverse-direction packet shows as To -> ... -> From.
170181 """
171182 subpath , _first , _last = _matched_subpath_with_indices (
172- hops , expected , max_hop_span , reversible
183+ hops , expected , max_hop_span , max_path_length , reversible
173184 )
174185 return subpath
175186
@@ -178,6 +189,7 @@ def _matched_subpath_with_indices(
178189 hops : list [dict [str , Any ]],
179190 expected : list [str ],
180191 max_hop_span : Optional [int ] = None ,
192+ max_path_length : Optional [int ] = None ,
181193 reversible : bool = True ,
182194) -> tuple [Optional [list [dict [str , Any ]]], Optional [int ], Optional [int ]]:
183195 """Variant of :func:`_matched_subpath` that also returns match indices.
@@ -189,11 +201,13 @@ def _matched_subpath_with_indices(
189201 ``route_recent_matches`` so the detail page can slice the live hop
190202 list without re-running the matcher.
191203 """
192- idx = _subsequence_indices (hops , expected , max_hop_span )
204+ idx = _subsequence_indices (hops , expected , max_hop_span , max_path_length )
193205 if idx is not None :
194206 return hops [idx [0 ] : idx [1 ] + 1 ], idx [0 ], idx [1 ]
195207 if reversible and len (expected ) > 1 :
196- idx = _subsequence_indices (hops , list (reversed (expected )), max_hop_span )
208+ idx = _subsequence_indices (
209+ hops , list (reversed (expected )), max_hop_span , max_path_length
210+ )
197211 if idx is not None :
198212 return hops [idx [0 ] : idx [1 ] + 1 ], idx [0 ], idx [1 ]
199213 return None , None , None
@@ -203,10 +217,14 @@ def _match_hops(
203217 hops : list [dict [str , Any ]],
204218 expected : list [str ],
205219 max_hop_span : Optional [int ] = None ,
220+ max_path_length : Optional [int ] = None ,
206221 reversible : bool = True ,
207222) -> bool :
208223 """Check whether *hops* match *expected* forward (and optionally reverse)."""
209- return _matched_subpath (hops , expected , max_hop_span , reversible ) is not None
224+ return (
225+ _matched_subpath (hops , expected , max_hop_span , max_path_length , reversible )
226+ is not None
227+ )
210228
211229
212230def _fetch_candidate_paths_maybe_bidirectional (
@@ -518,7 +536,9 @@ def evaluate_route(
518536
519537 matched_packets : set [str ] = set ()
520538 for hops in paths .values ():
521- if _match_hops (hops , expected , route .max_hop_span , reversible ):
539+ if _match_hops (
540+ hops , expected , route .max_hop_span , route .max_path_length , reversible
541+ ):
522542 identity = _match_identity (hops )
523543 if identity :
524544 matched_packets .add (identity )
@@ -571,7 +591,9 @@ def evaluate_route_day(
571591
572592 matched_packets : set [str ] = set ()
573593 for hops in paths .values ():
574- if _match_hops (hops , expected , route .max_hop_span , reversible ):
594+ if _match_hops (
595+ hops , expected , route .max_hop_span , route .max_path_length , reversible
596+ ):
575597 identity = _match_identity (hops )
576598 if identity :
577599 matched_packets .add (identity )
@@ -732,7 +754,9 @@ def evaluate_route_history(
732754 for i in range (historical_days ):
733755 matched_packets : set [str ] = set ()
734756 for hops in day_paths [i ].values ():
735- if _match_hops (hops , expected , route .max_hop_span , reversible ):
757+ if _match_hops (
758+ hops , expected , route .max_hop_span , route .max_path_length , reversible
759+ ):
736760 identity = _match_identity (hops )
737761 if identity :
738762 matched_packets .add (identity )
@@ -1129,7 +1153,11 @@ def recent_matches(
11291153 matches_by_identity : dict [str , dict [str , Any ]] = {}
11301154 for rp_id , hops in paths .items ():
11311155 subpath , first_idx , last_idx = _matched_subpath_with_indices (
1132- hops , expected , route .max_hop_span , reversible
1156+ hops ,
1157+ expected ,
1158+ route .max_hop_span ,
1159+ route .max_path_length ,
1160+ reversible ,
11331161 )
11341162 if not subpath or first_idx is None or last_idx is None :
11351163 continue
@@ -1232,13 +1260,14 @@ def preview_route(
12321260 """Preview matching for an unsaved route config.
12331261
12341262 *config* keys: ``node_ids``, ``match_width``, ``observer_ids``,
1235- ``max_hop_span``, ``packet_count_threshold ``, ``clear_threshold ``,
1236- ``reversible``.
1263+ ``max_hop_span``, ``max_path_length ``, ``packet_count_threshold ``,
1264+ ``clear_threshold``, `` reversible``.
12371265 """
12381266 node_ids : list [str ] = config .get ("node_ids" ) or []
12391267 match_width : int = config .get ("match_width" ) or 1
12401268 observer_ids : Optional [list [str ]] = config .get ("observer_ids" ) or None
12411269 max_hop_span : Optional [int ] = config .get ("max_hop_span" )
1270+ max_path_length : Optional [int ] = config .get ("max_path_length" )
12421271 threshold : int = config .get ("packet_count_threshold" ) or 5
12431272 clear_bar : Optional [int ] = config .get ("clear_threshold" )
12441273 reversible : bool = config .get ("reversible" , True )
@@ -1298,7 +1327,7 @@ def preview_route(
12981327 contributing : dict [str , int ] = {}
12991328
13001329 for hops in paths .values ():
1301- if _match_hops (hops , expected , max_hop_span , reversible ):
1330+ if _match_hops (hops , expected , max_hop_span , max_path_length , reversible ):
13021331 identity = _match_identity (hops )
13031332 if identity :
13041333 matched_packets .add (identity )
0 commit comments