diff --git a/.jules/sentinel.md b/.jules/sentinel.md
index 93201f0..0a6a29d 100644
--- a/.jules/sentinel.md
+++ b/.jules/sentinel.md
@@ -1,4 +1,4 @@
-## 2026-08-01 - Proper Handling of X-Forwarded-For in Rate Limiters
-**Vulnerability:** IP Spoofing via `X-Forwarded-For` header. The rate limiter incorrectly trusted the first IP in the `X-Forwarded-For` chain without verifying if the direct connection was from a trusted internal proxy. It also failed to parse the chain right-to-left, making it trivial for an external client to spoof an IP by prepending it to the header.
-**Learning:** Even when behind a reverse proxy, you cannot blindly trust `X-Forwarded-For`. An attacker can spoof it. You must verify that `request.getRemoteAddr()` (the direct connection) belongs to a trusted proxy. Furthermore, because legitimate proxies append to the end of the chain, you must parse the chain from right-to-left, skipping known internal proxies, to find the true client IP. Finally, be careful to default to the last trusted internal IP if no public IP is found in the chain, to avoid collapsing all internal traffic into a single rate-limit bucket.
-**Prevention:** Always use a right-to-left parsing strategy for proxy chains, strictly validating each hop against a whitelist of trusted internal IPs. If a connection doesn't originate from a trusted proxy, fallback immediately to `getRemoteAddr()`. Implement robust test cases that cover external connections, single proxies, multiple internal proxies, and internal clients traversing internal proxies.
+## 2026-09-11 - DoS Risk in Rate Limiter via Unbounded Memory
+**Vulnerability:** The RateLimitFilter stored client IP rate-limiting data in an unbounded `ConcurrentHashMap`. An attacker could spoof numerous IPs or target many arbitrary buckets to infinitely grow the map, leading to memory exhaustion (OOM) and DoS.
+**Learning:** Using basic Maps for caches or temporary tracking structures without strict bounds or eviction mechanisms in long-running applications poses significant DoS and stability risks.
+**Prevention:** Always use proper caching libraries (like Caffeine or Guava) with strict bounds (`maximumSize`) and automatic time-based eviction policies (`expireAfterAccess` or `expireAfterWrite`) when tracking dynamic client data in memory.
diff --git a/backend/pom.xml b/backend/pom.xml
index 79e1147..26085a9 100644
--- a/backend/pom.xml
+++ b/backend/pom.xml
@@ -46,6 +46,10 @@
org.springframework.boot
spring-boot-starter-actuator
+
+ com.github.ben-manes.caffeine
+ caffeine
+
diff --git a/backend/src/main/java/com/algorithmrace/visualizer/RateLimitFilter.java b/backend/src/main/java/com/algorithmrace/visualizer/RateLimitFilter.java
index b12da30..7e049b4 100644
--- a/backend/src/main/java/com/algorithmrace/visualizer/RateLimitFilter.java
+++ b/backend/src/main/java/com/algorithmrace/visualizer/RateLimitFilter.java
@@ -1,11 +1,14 @@
package com.algorithmrace.visualizer;
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.Ordered;
@@ -31,8 +34,11 @@ public class RateLimitFilter implements Filter {
private static final int DEFAULT_LIMIT = 60;
private static final long WINDOW_MS = 60_000L;
- // Stores: clientIP -> bucket -> [timestamps]
- private final Map> clients = new ConcurrentHashMap<>();
+ // Stores: clientIP -> bucket -> SlidingWindow
+ // Cache auto-evicts entire SlidingWindow objects 1 minute after their last access,
+ // preventing unbounded memory growth (OOM DoS) from many unique IPs or buckets.
+ private final Cache> clients =
+ Caffeine.newBuilder().expireAfterAccess(1, TimeUnit.MINUTES).maximumSize(100_000).build();
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
@@ -55,10 +61,8 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha
String bucket = resolveBucket(path);
int limit = resolveLimit(path);
- SlidingWindow window =
- clients
- .computeIfAbsent(clientIp, k -> new ConcurrentHashMap<>())
- .computeIfAbsent(bucket, k -> new SlidingWindow());
+ Map userBuckets = clients.get(clientIp, k -> new ConcurrentHashMap<>());
+ SlidingWindow window = userBuckets.computeIfAbsent(bucket, k -> new SlidingWindow());
if (!window.tryAcquire(limit)) {
log.warn("Rate limit exceeded for IP={} bucket={}", clientIp, bucket);
diff --git a/backend/src/main/java/com/algorithmrace/visualizer/algorithms/pathfinding/BellmanFordModel.java b/backend/src/main/java/com/algorithmrace/visualizer/algorithms/pathfinding/BellmanFordModel.java
index e3c1e52..163c381 100644
--- a/backend/src/main/java/com/algorithmrace/visualizer/algorithms/pathfinding/BellmanFordModel.java
+++ b/backend/src/main/java/com/algorithmrace/visualizer/algorithms/pathfinding/BellmanFordModel.java
@@ -1,19 +1,22 @@
package com.algorithmrace.visualizer.algorithms.pathfinding;
import java.util.ArrayDeque;
-import java.util.HashSet;
import java.util.Queue;
-import java.util.Set;
public class BellmanFordModel extends PathfindingModel {
private final Queue queue = new ArrayDeque<>();
- // O(1) lookup instead of O(n) queue.contains()
- private final Set inQueue = new HashSet<>();
+ private boolean[][] inQueue;
public BellmanFordModel() {
super("Bellman-Ford");
}
+ @Override
+ public void initGrid(int rows, int cols) {
+ super.initGrid(rows, cols);
+ inQueue = new boolean[rows][cols];
+ }
+
@Override
public void step() {
if (isDone() || queue.isEmpty()) {
@@ -25,7 +28,7 @@ public void step() {
}
GridCell current = queue.poll();
- inQueue.remove(current);
+ inQueue[current.row][current.col] = false;
if (current == end) {
reconstructPath(end);
@@ -46,9 +49,9 @@ public void step() {
if (nb.state == CellState.EMPTY) {
nb.state = CellState.FRONTIER;
}
- if (!inQueue.contains(nb)) {
+ if (!inQueue[nb.row][nb.col]) {
queue.add(nb);
- inQueue.add(nb);
+ inQueue[nb.row][nb.col] = true;
}
}
}
@@ -57,12 +60,20 @@ public void step() {
@Override
public void reset() {
queue.clear();
- inQueue.clear();
+ if (inQueue != null) {
+ for (int r = 0; r < rows; r++) {
+ for (int c = 0; c < cols; c++) {
+ inQueue[r][c] = false;
+ }
+ }
+ }
resetStats();
if (start != null) {
start.gCost = 0;
queue.add(start);
- inQueue.add(start);
+ if (inQueue != null) {
+ inQueue[start.row][start.col] = true;
+ }
}
}
}
diff --git a/backend/src/main/java/com/algorithmrace/visualizer/algorithms/pathfinding/JPSModel.java b/backend/src/main/java/com/algorithmrace/visualizer/algorithms/pathfinding/JPSModel.java
index 86671d5..a66ed4b 100644
--- a/backend/src/main/java/com/algorithmrace/visualizer/algorithms/pathfinding/JPSModel.java
+++ b/backend/src/main/java/com/algorithmrace/visualizer/algorithms/pathfinding/JPSModel.java
@@ -2,13 +2,16 @@
import java.util.ArrayList;
import java.util.Comparator;
+import java.util.HashSet;
import java.util.List;
import java.util.PriorityQueue;
+import java.util.Set;
public class JPSModel extends PathfindingModel {
private final PriorityQueue openSet =
new PriorityQueue<>(Comparator.comparingDouble(GridCell::fCost));
+ private final Set inOpenSet = new HashSet<>();
public JPSModel() {
super("Jump Point Search");
@@ -27,6 +30,9 @@ public void step() {
return;
}
GridCell current = openSet.poll();
+ if (current != null) {
+ inOpenSet.remove(current);
+ }
if (current == end) {
reconstructPath(end);
markDone();
@@ -49,8 +55,9 @@ public void step() {
if (nb.state == CellState.EMPTY || nb.state == CellState.VISITED) {
nb.state = CellState.FRONTIER;
}
- if (!openSet.contains(nb)) {
+ if (!inOpenSet.contains(nb)) {
openSet.add(nb);
+ inOpenSet.add(nb);
}
}
}
@@ -232,10 +239,12 @@ protected void reconstructPath(GridCell endCell) {
@Override
public void reset() {
openSet.clear();
+ inOpenSet.clear();
resetStats();
if (start != null) {
start.gCost = 0;
openSet.add(start);
+ inOpenSet.add(start);
}
}
}
diff --git a/backend/src/main/java/com/algorithmrace/visualizer/algorithms/pathfinding/JPSModel.java.orig b/backend/src/main/java/com/algorithmrace/visualizer/algorithms/pathfinding/JPSModel.java.orig
new file mode 100644
index 0000000..a89cfb7
--- /dev/null
+++ b/backend/src/main/java/com/algorithmrace/visualizer/algorithms/pathfinding/JPSModel.java.orig
@@ -0,0 +1,257 @@
+package com.algorithmrace.visualizer.algorithms.pathfinding;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.PriorityQueue;
+import java.util.Set;
+
+public class JPSModel extends PathfindingModel {
+
+ private final PriorityQueue openSet =
+ new PriorityQueue<>(Comparator.comparingDouble(GridCell::fCost));
+ private final Set openSetContains = new HashSet<>();
+ private final Set inOpenSet = new HashSet<>();
+
+ public JPSModel() {
+ super("Jump Point Search");
+ }
+
+ private double heuristic(GridCell a, GridCell b) {
+ int dx = Math.abs(a.row - b.row);
+ int dy = Math.abs(a.col - b.col);
+ return Math.max(dx, dy) + (Math.sqrt(2) - 1) * Math.min(dx, dy);
+ }
+
+ @Override
+ public void step() {
+ if (isDone() || openSet.isEmpty()) {
+ markDone();
+ return;
+ }
+ GridCell current = openSet.poll();
+ openSetContains.remove(current);
+ if (current != null) {
+ inOpenSet.remove(current);
+ }
+ if (current == end) {
+ reconstructPath(end);
+ markDone();
+ return;
+ }
+ if (current.state != CellState.START) {
+ current.state = CellState.VISITED;
+ }
+ addStep();
+
+ List successors = identifySuccessors(current);
+ for (GridCell nb : successors) {
+ double dist =
+ Math.sqrt(Math.pow(current.row - nb.row, 2) + Math.pow(current.col - nb.col, 2));
+ double tentativeG = current.gCost + dist;
+ if (tentativeG < nb.gCost) {
+ nb.gCost = tentativeG;
+ nb.hCost = heuristic(nb, end);
+ nb.parent = current;
+ if (nb.state == CellState.EMPTY || nb.state == CellState.VISITED) {
+ nb.state = CellState.FRONTIER;
+ }
+ if (!openSetContains.contains(nb)) {
+ openSet.add(nb);
+ openSetContains.add(nb);
+ if (!inOpenSet.contains(nb)) {
+ openSet.add(nb);
+ inOpenSet.add(nb);
+ }
+ }
+ }
+ }
+
+ private List identifySuccessors(GridCell current) {
+ List successors = new ArrayList<>();
+ List neighbors = getPrunedNeighbors(current);
+ for (GridCell neighbor : neighbors) {
+ int dRow = neighbor.row - current.row;
+ int dCol = neighbor.col - current.col;
+ GridCell jumpPoint = jump(current.row, current.col, dRow, dCol);
+ if (jumpPoint != null) {
+ successors.add(jumpPoint);
+ }
+ }
+ return successors;
+ }
+
+ private List getPrunedNeighbors(GridCell current) {
+ List neighbors = new ArrayList<>();
+ if (current.parent == null) {
+ for (int dr = -1; dr <= 1; dr++) {
+ for (int dc = -1; dc <= 1; dc++) {
+ if (dr == 0 && dc == 0) continue;
+ if (isValid(current.row + dr, current.col + dc)) {
+ if (dr != 0 && dc != 0) {
+ if (isValid(current.row + dr, current.col)
+ || isValid(current.row, current.col + dc)) {
+ neighbors.add(grid[current.row + dr][current.col + dc]);
+ }
+ } else {
+ neighbors.add(grid[current.row + dr][current.col + dc]);
+ }
+ }
+ }
+ }
+ return neighbors;
+ }
+
+ int dRow = Integer.compare(current.row, current.parent.row);
+ int dCol = Integer.compare(current.col, current.parent.col);
+
+ if (dRow != 0 && dCol != 0) {
+ boolean vRow = isValid(current.row + dRow, current.col);
+ boolean vCol = isValid(current.row, current.col + dCol);
+ if (vRow) neighbors.add(grid[current.row + dRow][current.col]);
+ if (vCol) neighbors.add(grid[current.row][current.col + dCol]);
+ if (vRow || vCol) {
+ if (isValid(current.row + dRow, current.col + dCol)) {
+ neighbors.add(grid[current.row + dRow][current.col + dCol]);
+ }
+ }
+ if (!isValid(current.row - dRow, current.col) && vCol) {
+ if (isValid(current.row - dRow, current.col + dCol)) {
+ neighbors.add(grid[current.row - dRow][current.col + dCol]);
+ }
+ }
+ if (!isValid(current.row, current.col - dCol) && vRow) {
+ if (isValid(current.row + dRow, current.col - dCol)) {
+ neighbors.add(grid[current.row + dRow][current.col - dCol]);
+ }
+ }
+ } else {
+ if (dRow != 0) {
+ if (isValid(current.row + dRow, current.col)) {
+ neighbors.add(grid[current.row + dRow][current.col]);
+ if (!isValid(current.row, current.col + 1)) {
+ if (isValid(current.row + dRow, current.col + 1)) {
+ neighbors.add(grid[current.row + dRow][current.col + 1]);
+ }
+ }
+ if (!isValid(current.row, current.col - 1)) {
+ if (isValid(current.row + dRow, current.col - 1)) {
+ neighbors.add(grid[current.row + dRow][current.col - 1]);
+ }
+ }
+ }
+ } else {
+ if (isValid(current.row, current.col + dCol)) {
+ neighbors.add(grid[current.row][current.col + dCol]);
+ if (!isValid(current.row + 1, current.col)) {
+ if (isValid(current.row + 1, current.col + dCol)) {
+ neighbors.add(grid[current.row + 1][current.col + dCol]);
+ }
+ }
+ if (!isValid(current.row - 1, current.col)) {
+ if (isValid(current.row - 1, current.col + dCol)) {
+ neighbors.add(grid[current.row - 1][current.col + dCol]);
+ }
+ }
+ }
+ }
+ }
+ return neighbors;
+ }
+
+ private boolean isValid(int r, int c) {
+ return r >= 0 && r < rows && c >= 0 && c < cols && grid[r][c].state != CellState.WALL;
+ }
+
+ private GridCell jump(int r, int c, int dRow, int dCol) {
+ while (true) {
+ int nextR = r + dRow;
+ int nextC = c + dCol;
+
+ if (!isValid(nextR, nextC)) {
+ return null;
+ }
+
+ if (dRow != 0 && dCol != 0) {
+ if (!isValid(r + dRow, c) && !isValid(r, c + dCol)) {
+ return null;
+ }
+ }
+
+ GridCell nextCell = grid[nextR][nextC];
+ if (nextCell == end) {
+ return nextCell;
+ }
+
+ if (nextCell.state == CellState.EMPTY) {
+ nextCell.state = CellState.VISITED;
+ }
+
+ if (dRow != 0 && dCol != 0) {
+ if ((!isValid(nextR - dRow, nextC) && isValid(nextR - dRow, nextC + dCol))
+ || (!isValid(nextR, nextC - dCol) && isValid(nextR + dRow, nextC - dCol))) {
+ return nextCell;
+ }
+ if (jump(nextR, nextC, dRow, 0) != null || jump(nextR, nextC, 0, dCol) != null) {
+ return nextCell;
+ }
+ } else {
+ if (dRow != 0) {
+ if ((!isValid(nextR, nextC + 1) && isValid(nextR + dRow, nextC + 1))
+ || (!isValid(nextR, nextC - 1) && isValid(nextR + dRow, nextC - 1))) {
+ return nextCell;
+ }
+ } else {
+ if ((!isValid(nextR + 1, nextC) && isValid(nextR + 1, nextC + dCol))
+ || (!isValid(nextR - 1, nextC) && isValid(nextR - 1, nextC + dCol))) {
+ return nextCell;
+ }
+ }
+ }
+ r = nextR;
+ c = nextC;
+ }
+ }
+
+ @Override
+ protected void reconstructPath(GridCell endCell) {
+ path.clear();
+ GridCell current = endCell;
+ while (current != null && current.parent != null) {
+ GridCell parent = current.parent;
+ int r = current.row;
+ int c = current.col;
+ int pr = parent.row;
+ int pc = parent.col;
+
+ int dRow = Integer.compare(r, pr);
+ int dCol = Integer.compare(c, pc);
+
+ while (r != pr || c != pc) {
+ path.add(0, grid[r][c]);
+ r -= dRow;
+ c -= dCol;
+ }
+ current = parent;
+ }
+ if (current != null) {
+ path.add(0, current);
+ }
+ pathFound = true;
+ }
+
+ @Override
+ public void reset() {
+ openSet.clear();
+ openSetContains.clear();
+ inOpenSet.clear();
+ resetStats();
+ if (start != null) {
+ start.gCost = 0;
+ openSet.add(start);
+ openSetContains.add(start);
+ inOpenSet.add(start);
+ }
+ }
+}
diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml
index d982810..1c0e3ff 100644
--- a/backend/src/main/resources/application.yml
+++ b/backend/src/main/resources/application.yml
@@ -23,6 +23,9 @@ springdoc:
path: /swagger-ui.html
management:
+ server:
+ port: 8081
+ address: 127.0.0.1
endpoints:
web:
exposure:
diff --git a/frontend/src/data/algorithmCodeSnippets.ts b/frontend/src/data/algorithmCodeSnippets.ts
index c565ced..8c668ee 100644
--- a/frontend/src/data/algorithmCodeSnippets.ts
+++ b/frontend/src/data/algorithmCodeSnippets.ts
@@ -612,7 +612,7 @@ function merge(L: number[], R: number[]): number[] {
code: `function binarySearch(arr: number[], target: number): number {
let lo = 0, hi = arr.length - 1;
while (lo <= hi) { // loop
- const mid = Math.floor((lo + hi) / 2);
+ const mid = lo + Math.floor((hi - lo) / 2);
if (arr[mid] === target) return mid; // found!
if (arr[mid] < target) lo = mid + 1; // narrow right
else hi = mid - 1; // narrow left
@@ -640,7 +640,7 @@ function merge(L: number[], R: number[]): number[] {
code: `def binary_search(arr, target):
lo, hi = 0, len(arr) - 1
while lo <= hi: # loop
- mid = (lo + hi) // 2
+ mid = lo + (hi - lo) // 2
if arr[mid] == target: return mid # found!
elif arr[mid] < target: lo = mid + 1 # narrow right
else: hi = mid - 1 # narrow left
diff --git a/frontend/src/data/algorithmMetadata.ts b/frontend/src/data/algorithmMetadata.ts
index e1de2aa..dcf4589 100644
--- a/frontend/src/data/algorithmMetadata.ts
+++ b/frontend/src/data/algorithmMetadata.ts
@@ -365,7 +365,7 @@ export const algorithmMetadata: Record= wt; w--) {
dp[w] = Math.max(dp[w], dp[w - wt] + val);
}
}
diff --git a/frontend/src/pages/DPPage.tsx b/frontend/src/pages/DPPage.tsx
index f1834f9..6920dfc 100644
--- a/frontend/src/pages/DPPage.tsx
+++ b/frontend/src/pages/DPPage.tsx
@@ -1,5 +1,5 @@
import React, { useState, useEffect, useRef } from 'react';
-import { Layers, Plus, Trash2, CheckCircle2, ListOrdered, FileText, Info, ShieldCheck } from 'lucide-react';
+import { Layers, Plus, Trash2, CheckCircle2, Info, ShieldCheck } from 'lucide-react';
import { DPCanvas, DPStep } from '../components/DPCanvas';
import { CodeViewer } from '../components/CodeViewer';
import { Controls } from '../components/Controls';
diff --git a/frontend/src/pages/SortingPage.tsx b/frontend/src/pages/SortingPage.tsx
index ef744b2..727bae3 100644
--- a/frontend/src/pages/SortingPage.tsx
+++ b/frontend/src/pages/SortingPage.tsx
@@ -19,7 +19,7 @@ import { StepExplanationCard } from '../components/StepExplanationCard';
import { CustomDatasetModal } from '../components/CustomDatasetModal';
import { ShareBenchmarkModal } from '../components/ShareBenchmarkModal';
import { CsvUploader } from '../components/CsvUploader';
-import { Share2, Cpu, Zap, Sparkles } from 'lucide-react';
+import { Share2, Cpu } from 'lucide-react';
import { getUrlParams } from '../utils/urlParams';
import { parseCurrentShareableConfig } from '../utils/shareableBenchmark';
import { workerSimulationService } from '../services/workerSimulationService';
diff --git a/frontend/src/workers/simulationWorker.ts b/frontend/src/workers/simulationWorker.ts
index d1b104d..865674d 100644
--- a/frontend/src/workers/simulationWorker.ts
+++ b/frontend/src/workers/simulationWorker.ts
@@ -535,7 +535,7 @@ function simulateSingleSearchingAlgorithm(
let right = arr.length - 1;
while (left <= right) {
- const mid = Math.floor((left + right) / 2);
+ const mid = left + Math.floor((right - left) / 2);
comparisons++;
steps++;
diff --git a/patch3.diff b/patch3.diff
new file mode 100644
index 0000000..4df2424
--- /dev/null
+++ b/patch3.diff
@@ -0,0 +1,39 @@
+--- backend/src/main/java/com/algorithmrace/visualizer/algorithms/pathfinding/JPSModel.java
++++ backend/src/main/java/com/algorithmrace/visualizer/algorithms/pathfinding/JPSModel.java
+@@ -58,15 +58,10 @@
+ if (nb.state == CellState.EMPTY || nb.state == CellState.VISITED) {
+ nb.state = CellState.FRONTIER;
+ }
+-<<<<<<< HEAD
+- if (!openSetContains.contains(nb)) {
+- openSet.add(nb);
+- openSetContains.add(nb);
+-=======
+ if (!inOpenSet.contains(nb)) {
+ openSet.add(nb);
+ inOpenSet.add(nb);
+->>>>>>> origin/master
+ }
+ }
+ }
+@@ -236,19 +231,11 @@
+ @Override
+ public void reset() {
+ openSet.clear();
+-<<<<<<< HEAD
+- openSetContains.clear();
+-=======
+ inOpenSet.clear();
+->>>>>>> origin/master
+ resetStats();
+ if (start != null) {
+ start.gCost = 0;
+ openSet.add(start);
+-<<<<<<< HEAD
+- openSetContains.add(start);
+-=======
+ inOpenSet.add(start);
+->>>>>>> origin/master
+ }
+ }
+ }
diff --git a/patch4.diff b/patch4.diff
new file mode 100644
index 0000000..698d3df
--- /dev/null
+++ b/patch4.diff
@@ -0,0 +1,43 @@
+--- backend/src/main/java/com/algorithmrace/visualizer/algorithms/pathfinding/JPSModel.java
++++ backend/src/main/java/com/algorithmrace/visualizer/algorithms/pathfinding/JPSModel.java
+@@ -10,7 +10,6 @@
+
+ private final PriorityQueue openSet =
+ new PriorityQueue<>(Comparator.comparingDouble(GridCell::fCost));
+- private final Set openSetContains = new HashSet<>();
+ private final Set inOpenSet = new HashSet<>();
+
+ public JPSModel() {
+@@ -30,7 +29,6 @@
+ return;
+ }
+ GridCell current = openSet.poll();
+- openSetContains.remove(current);
+ if (current != null) {
+ inOpenSet.remove(current);
+ }
+@@ -58,9 +56,6 @@
+ if (nb.state == CellState.EMPTY || nb.state == CellState.VISITED) {
+ nb.state = CellState.FRONTIER;
+ }
+- if (!openSetContains.contains(nb)) {
+- openSet.add(nb);
+- openSetContains.add(nb);
+ if (!inOpenSet.contains(nb)) {
+ openSet.add(nb);
+ inOpenSet.add(nb);
+@@ -237,7 +232,6 @@
+ @Override
+ public void reset() {
+ openSet.clear();
+- openSetContains.clear();
+ inOpenSet.clear();
+ resetStats();
+ if (start != null) {
+@@ -245,7 +239,6 @@
+ openSet.add(start);
+- openSetContains.add(start);
+ inOpenSet.add(start);
+ }
+ }
+ }