Skip to content

Commit 3861789

Browse files
Merge branch 'main' into fix/URLSanitizer
2 parents b5e3ba9 + 5f371d0 commit 3861789

7 files changed

Lines changed: 743 additions & 653 deletions

File tree

math/README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,24 @@
33
This folder contains Python mini-projects focused on math concepts, number patterns, calculators, and transformations. Each project is in its own subfolder and can be run directly from the terminal.
44

55
## Projects in `math/`
6-
76
- AP-GP-AGP-HP-Recognizer
87
- Armstrong-Number
8+
- Binary-Search
9+
- Bubble-Sort
910
- Collatz-Conjecture
11+
- Complete-Calculus-Engine
1012
- Coordinate-to-Polar-Transformation
1113
- Derivative-Calculator
1214
- Fibonacci-Series
15+
- Fourier-Series-Visualizer
1316
- Happy-Number
1417
- Matrix-Calculator
18+
- Merge-Sort
1519
- Pascal-Triangle
1620
- Prime-Number-Analyzer
1721
- Projectile-Motion-Game
22+
- Quadratic-Solver
23+
- Quick-Sort
1824
- Scientific-Graphing-Calculator
1925

2026
## How to Set Up and Run Any Math Project

security/tar_safe.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,10 @@ def _extract_file(
197197
while bytes_copied < member.size:
198198
chunk = src.read(min(chunk_size, member.size - bytes_copied))
199199
if not chunk:
200-
break
200+
raise UnsafeTarError(
201+
f"Truncated file: {member.name} - expected {member.size} bytes, "
202+
f"got {bytes_copied}"
203+
)
201204
bytes_copied += len(chunk)
202205
tmp.write(chunk)
203206

security/url_sanitizer.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,21 @@ def _validate_port(self, port: Optional[int]) -> None:
155155
elif port in self.BLOCKED_PORTS:
156156
raise InvalidURLError(f"Blocked port: {port}")
157157

158+
def _validate_port(self, port: Optional[int]) -> None:
159+
"""Validate port number against allowed/blocked lists."""
160+
if port is None:
161+
return
162+
if port < 1 or port > 65535:
163+
raise InvalidURLError(f"Invalid port number: {port}")
164+
if self.allowed_ports is not None:
165+
if port not in self.allowed_ports:
166+
raise InvalidURLError(
167+
f"Port {port} is not allowed. "
168+
f"Allowed ports: {', '.join(str(p) for p in self.allowed_ports)}"
169+
)
170+
elif port in self.BLOCKED_PORTS:
171+
raise InvalidURLError(f"Blocked port: {port}")
172+
158173
def _validate_characters(self, url: str) -> None:
159174
"""Check for unsafe or malicious characters."""
160175
if '\0' in url:

tests/test_security.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,27 @@ def test_nested_valid_directories(self, tmp_path):
263263
assert len(extracted) == 1
264264
assert (extract_dir / 'dir1' / 'dir2' / 'file.txt').exists()
265265

266+
def test_truncated_file_detection(self, tmp_path):
267+
"""Test that truncated files (size metadata > actual data) are detected."""
268+
tar_file = tmp_path / "truncated.tar"
269+
270+
with tarfile.open(tar_file, 'w') as tar:
271+
info = tarfile.TarInfo(name='truncated.txt')
272+
info.size = 1000
273+
info.type = tarfile.REGTYPE
274+
content = b'x' * 1000
275+
fileobj = io.BytesIO(content)
276+
tar.addfile(info, fileobj)
277+
278+
data = tar_file.read_bytes()
279+
tar_file.write_bytes(data[:512 + 500])
280+
281+
extract_dir = tmp_path / "extracted"
282+
extractor = SafeTarExtractor()
283+
284+
with pytest.raises(UnsafeTarError):
285+
extractor.extract(tar_file, extract_dir)
286+
266287
def test_size_limits(self, tmp_path):
267288
"""Test file size limits."""
268289
tar_file = tmp_path / "large.tar"

utils/validation.py

Lines changed: 13 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ def get_int(
1313
val_str = input(prompt).strip()
1414
if not val_str:
1515
if default is not None:
16+
if min_value is not None and default < min_value:
17+
print(f"❌ Default {default} is below minimum {min_value}.")
18+
continue
19+
if max_value is not None and default > max_value:
20+
print(f"❌ Default {default} is above maximum {max_value}.")
21+
continue
1622
return default
1723
print(error_empty)
1824
continue
@@ -40,6 +46,12 @@ def get_float(
4046
val_str = input(prompt).strip()
4147
if not val_str:
4248
if default is not None:
49+
if min_value is not None and default < min_value:
50+
print(f"❌ Default {default} is below minimum {min_value}.")
51+
continue
52+
if max_value is not None and default > max_value:
53+
print(f"❌ Default {default} is above maximum {max_value}.")
54+
continue
4355
return default
4456
print(error_empty)
4557
continue
@@ -52,103 +64,4 @@ def get_float(
5264
continue
5365
return val
5466
except ValueError:
55-
print(error_invalid)
56-
57-
def get_non_empty_string(
58-
prompt: str,
59-
default: Optional[str] = None,
60-
error_empty: str = "❌ Error: Input cannot be empty.",
61-
) -> str:
62-
while True:
63-
val_str = input(prompt).strip()
64-
if not val_str:
65-
if default is not None:
66-
return default
67-
print(error_empty)
68-
continue
69-
return val_str
70-
71-
def get_choice(
72-
prompt: str,
73-
choices: List[str],
74-
default: Optional[str] = None,
75-
error_empty: str = "❌ Error: Input cannot be empty.",
76-
error_invalid: Optional[str] = None,
77-
) -> str:
78-
choices_lower = [c.lower() for c in choices]
79-
while True:
80-
val_str = input(prompt).strip()
81-
if not val_str:
82-
if default is not None:
83-
return default
84-
print(error_empty)
85-
continue
86-
if val_str.lower() in choices_lower:
87-
idx = choices_lower.index(val_str.lower())
88-
return choices[idx]
89-
if error_invalid is not None:
90-
print(error_invalid)
91-
else:
92-
print(f"❌ Invalid selection. Please choose from: {', '.join(choices)}")
93-
94-
def get_yes_no(prompt: str, default: Optional[str] = None) -> bool:
95-
while True:
96-
val_str = input(prompt).strip().lower()
97-
if not val_str:
98-
if default is not None:
99-
return default.lower() in ['y', 'yes']
100-
print("❌ Error: Input cannot be empty. Please enter 'y' or 'n'.")
101-
continue
102-
if val_str in ['y', 'yes']:
103-
return True
104-
if val_str in ['n', 'no']:
105-
return False
106-
print("❌ Invalid choice. Please enter 'y' or 'n'.")
107-
108-
def get_int_list(
109-
prompt: str,
110-
min_len: Optional[int] = None,
111-
max_len: Optional[int] = None,
112-
error_empty: str = "❌ Error: Input cannot be empty.",
113-
error_invalid: str = "❌ Error: Please enter valid integers only.",
114-
) -> List[int]:
115-
while True:
116-
val_str = input(prompt).strip()
117-
if not val_str:
118-
print(error_empty)
119-
continue
120-
try:
121-
val_list = [int(x) for x in val_str.split()]
122-
if min_len is not None and len(val_list) < min_len:
123-
print(f"❌ Error: Please enter at least {min_len} numbers.")
124-
continue
125-
if max_len is not None and len(val_list) > max_len:
126-
print(f"❌ Error: Please enter at most {max_len} numbers.")
127-
continue
128-
return val_list
129-
except ValueError:
130-
print(error_invalid)
131-
132-
def get_float_list(
133-
prompt: str,
134-
min_len: Optional[int] = None,
135-
max_len: Optional[int] = None,
136-
error_empty: str = "❌ Error: Input cannot be empty.",
137-
error_invalid: str = "❌ Error: Please enter valid numbers only.",
138-
) -> List[float]:
139-
while True:
140-
val_str = input(prompt).strip()
141-
if not val_str:
142-
print(error_empty)
143-
continue
144-
try:
145-
val_list = [float(x) for x in val_str.split()]
146-
if min_len is not None and len(val_list) < min_len:
147-
print(f"❌ Error: Please enter at least {min_len} numbers.")
148-
continue
149-
if max_len is not None and len(val_list) > max_len:
150-
print(f"❌ Error: Please enter at most {max_len} numbers.")
151-
continue
152-
return val_list
153-
except ValueError:
154-
print(error_invalid)
67+
print(error_invalid)

0 commit comments

Comments
 (0)