-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinput_validation.py
More file actions
290 lines (232 loc) · 10.8 KB
/
input_validation.py
File metadata and controls
290 lines (232 loc) · 10.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
"""
Input Validation System for AW-RPC
Prevents crashes and ensures data integrity
"""
import re
from functools import wraps
from typing import Tuple, List, Any
from enhanced_logging import log_error
class ValidationError(Exception):
"""Custom exception for validation errors"""
pass
class InputValidator:
"""Centralized input validation"""
# Valid game constants
VALID_ARMIES = {'RED', 'BLUE', 'GREEN', 'YELLOW', 'GREY'}
VALID_UNIT_TYPES = {
'INFANTRY', 'MECH', 'RECON', 'TANK', 'MEDIUMTANK', 'NEOTANK', 'MEGATANK',
'ARTILLERY', 'MISSILE', 'ROCKET', 'ANTIAIR', 'APC', 'FIGHTER', 'BOMBER',
'BCOPTER', 'TCOPTER', 'BATTLESHIP', 'CRUISER', 'LANDER', 'SUB', 'CARRIER',
'BLACKBOAT', 'STEALTH', 'BLACKBOMB', 'PIPERUNNER'
}
@staticmethod
def validate_token(token: str) -> str:
"""Validate game token format"""
if not token or not isinstance(token, str):
raise ValidationError("Token must be a non-empty string")
# Remove whitespace
token = token.strip()
# Check length
if len(token) < 3 or len(token) > 50:
raise ValidationError("Token must be 3-50 characters long")
# Check format - alphanumeric, underscore, dash only
if not re.match(r'^[a-zA-Z0-9_-]+$', token):
raise ValidationError("Token contains invalid characters. Use only letters, numbers, - and _")
return token
@staticmethod
def validate_coordinates(x: Any, y: Any, max_x: int = 50, max_y: int = 50) -> Tuple[int, int]:
"""Validate and convert coordinates to integers"""
try:
x, y = int(x), int(y)
except (ValueError, TypeError):
raise ValidationError(f"Coordinates must be integers, got x={type(x).__name__}, y={type(y).__name__}")
if x < 0 or y < 0:
raise ValidationError(f"Coordinates cannot be negative: ({x},{y})")
if x >= max_x or y >= max_y:
raise ValidationError(f"Coordinates ({x},{y}) out of bounds (max: {max_x-1},{max_y-1})")
return x, y
@staticmethod
def validate_army(army: Any) -> str:
"""Validate army parameter"""
if not isinstance(army, str):
raise ValidationError(f"Army must be a string, got {type(army).__name__}")
army = army.upper().strip()
if army not in InputValidator.VALID_ARMIES:
raise ValidationError(f"Invalid army '{army}'. Valid armies: {sorted(InputValidator.VALID_ARMIES)}")
return army
@staticmethod
def validate_unit_type(unit_type: Any) -> str:
"""Validate unit type parameter"""
if not isinstance(unit_type, str):
raise ValidationError(f"Unit type must be a string, got {type(unit_type).__name__}")
unit_type = unit_type.upper().strip()
if unit_type not in InputValidator.VALID_UNIT_TYPES:
raise ValidationError(f"Invalid unit type '{unit_type}'. See /api/browse for valid types")
return unit_type
@staticmethod
def validate_index(index: Any, max_value: int = 10) -> int:
"""Validate array index parameter"""
try:
index = int(index)
except (ValueError, TypeError):
raise ValidationError(f"Index must be an integer, got {type(index).__name__}")
if index < 0:
raise ValidationError(f"Index cannot be negative: {index}")
if index >= max_value:
raise ValidationError(f"Index {index} too large (max: {max_value-1})")
return index
# Validation decorators for RPC methods
def validate_token_param(func):
"""Decorator to validate token parameter"""
@wraps(func)
def wrapper(*args, **kwargs):
# Token is always the first parameter
if args:
token = args[0]
try:
validated_token = InputValidator.validate_token(token)
args = (validated_token,) + args[1:]
except ValidationError as e:
log_error(token, func.__name__, str(e), "Token validation failed")
raise
elif 'token' in kwargs:
try:
kwargs['token'] = InputValidator.validate_token(kwargs['token'])
except ValidationError as e:
log_error(kwargs.get('token', 'invalid'), func.__name__, str(e), "Token validation failed")
raise
return func(*args, **kwargs)
return wrapper
def validate_coordinates_params(func):
"""Decorator to validate coordinate parameters"""
@wraps(func)
def wrapper(*args, **kwargs):
token = args[0] if args else kwargs.get('token', 'unknown')
try:
# Handle positional arguments
if len(args) >= 3: # token, x, y at minimum
x, y = InputValidator.validate_coordinates(args[1], args[2])
args = (args[0], x, y) + args[3:]
# Handle additional coordinate pairs (x2, y2)
if len(args) >= 5: # token, x, y, x2, y2
x2, y2 = InputValidator.validate_coordinates(args[3], args[4])
args = args[:3] + (x2, y2) + args[5:]
# Handle keyword arguments
if 'x' in kwargs and 'y' in kwargs:
kwargs['x'], kwargs['y'] = InputValidator.validate_coordinates(kwargs['x'], kwargs['y'])
if 'x2' in kwargs and 'y2' in kwargs:
kwargs['x2'], kwargs['y2'] = InputValidator.validate_coordinates(kwargs['x2'], kwargs['y2'])
except ValidationError as e:
log_error(token, func.__name__, str(e), "Coordinate validation failed")
raise
return func(*args, **kwargs)
return wrapper
def validate_army_param(func):
"""Decorator to validate army parameter"""
@wraps(func)
def wrapper(*args, **kwargs):
token = args[0] if args else kwargs.get('token', 'unknown')
try:
# Look for army in args (usually 2nd parameter after token)
if len(args) >= 2 and isinstance(args[1], str) and args[1].upper() in InputValidator.VALID_ARMIES | {'RED', 'BLUE'}:
army = InputValidator.validate_army(args[1])
args = (args[0], army) + args[2:]
# Look for army in kwargs
if 'army' in kwargs:
kwargs['army'] = InputValidator.validate_army(kwargs['army'])
except ValidationError as e:
log_error(token, func.__name__, str(e), "Army validation failed")
raise
return func(*args, **kwargs)
return wrapper
def validate_unit_type_param(func):
"""Decorator to validate unit_type parameter"""
@wraps(func)
def wrapper(*args, **kwargs):
token = args[0] if args else kwargs.get('token', 'unknown')
try:
# Look for unit_type in args (usually 3rd parameter: token, army, unit_type)
if len(args) >= 3 and isinstance(args[2], str):
unit_type = InputValidator.validate_unit_type(args[2])
args = args[:2] + (unit_type,) + args[3:]
# Look for unit_type in kwargs
if 'unit_type' in kwargs:
kwargs['unit_type'] = InputValidator.validate_unit_type(kwargs['unit_type'])
except ValidationError as e:
log_error(token, func.__name__, str(e), "Unit type validation failed")
raise
return func(*args, **kwargs)
return wrapper
def validate_all_params(func):
"""Comprehensive validation decorator - use this for most RPC methods"""
@validate_token_param
@validate_coordinates_params
@validate_army_param
@validate_unit_type_param
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
# Game state validation functions
def validate_game_active(manager):
"""Ensure game is still active"""
if not getattr(manager.board, 'game_active', True):
raise ValidationError("Game has ended. No further moves allowed.")
def validate_unit_exists(manager, x: int, y: int):
"""Ensure unit exists at coordinates"""
unit = manager.unit_at(x, y)
if not unit:
raise ValidationError(f"No unit found at coordinates ({x},{y})")
return unit
def validate_player_turn(manager, unit):
"""Ensure it's the correct player's turn"""
if unit.army != manager.board.current_turn:
raise ValidationError(f"It's {manager.board.current_turn.name}'s turn, not {unit.army.name}'s")
def validate_unit_can_act(unit, action: str):
"""Ensure unit can perform the requested action"""
if action == "move" and not unit.can_move:
raise ValidationError(f"{unit.type.name} cannot move this turn")
elif action == "attack" and not unit.can_attack:
raise ValidationError(f"{unit.type.name} cannot attack this turn")
elif action == "capture" and not unit.can_capture:
raise ValidationError(f"{unit.type.name} cannot capture this turn")
# Enhanced validation decorator for game manager methods
def validate_game_action(action_type: str):
"""Decorator for GameManager methods to validate game state"""
def decorator(func):
@wraps(func)
def wrapper(self, x: int, y: int, *args, **kwargs):
try:
# Basic game state validation
validate_game_active(self)
# Coordinate validation (already done by coordinate decorator)
# Unit existence validation
unit = validate_unit_exists(self, x, y)
# Turn validation
validate_player_turn(self, unit)
# Action-specific validation
if action_type in ["move", "attack", "capture"]:
validate_unit_can_act(unit, action_type)
return func(self, x, y, *args, **kwargs)
except ValidationError:
raise # Re-raise validation errors as-is
except Exception as e:
log_error(
getattr(self, 'current_token', 'unknown'),
func.__name__,
str(e),
f"Unexpected error during {action_type}"
)
raise
return wrapper
return decorator
# Example usage in manager.py:
# @validate_game_action("move")
# def unit_move(self, x: int, y: int, x2: int, y2: int) -> Unit:
# # Your existing logic here
# pass
# Example usage in app.py:
# @validate_all_params
# def unit_create_rpc(token: str, army: str, unit_type: str, x: int, y: int) -> dict:
# # Your existing logic here
# pass