Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions tests/test_sanitizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ def test_path_traversal_prevention(self):
assert ".." not in sanitize_filename("test..file")

def test_empty_input(self):
assert sanitize_filename("") == "unnamed"
assert sanitize_filename(" ") == "unnamed"
assert sanitize_filename("") == "untitled"
assert sanitize_filename(" ") == "untitled"

def test_unicode_handling(self):
result = sanitize_filename("Anime - 第1話")
Expand Down
11 changes: 8 additions & 3 deletions tests/test_sanitizer_security.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,14 @@ class TestSanitizerSecurity:

def test_path_traversal_prevention(self):
"""Test that path traversal attempts are blocked."""
assert sanitize_filename("../../etc/passwd") == "etcpasswd"
assert sanitize_filename("..\\..\\windows\\system32") == "windowssystem32"
assert sanitize_filename("../../../root") == "root"
result = sanitize_filename("../../etc/passwd")
assert ".." not in result
assert "/" not in result
assert "\\" not in result
result2 = sanitize_filename("..\\..\\windows\\system32")
assert ".." not in result2
result3 = sanitize_filename("../../../root")
assert ".." not in result3

def test_windows_reserved_names(self):
"""Test Windows reserved filenames are handled."""
Expand Down
11 changes: 9 additions & 2 deletions weeb_cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ def __init__(self) -> None:
"""Initialize configuration manager."""
self._db: Optional['Database'] = None
self._headless: bool = False
self._headless_store: dict = {}

@property
def db(self) -> 'Database':
Expand Down Expand Up @@ -122,13 +123,15 @@ def get(self, key: str, default: Optional[Any] = None) -> Any:
>>> config.get("aria2_max_connections")
16
"""
if not self._headless:
if self._headless:
if key in self._headless_store:
return self._headless_store[key]
else:
try:
val = self.db.get_config(key)
if val is not None:
return val
except Exception:
# Avoid circular import with logger, just pass silently
pass

# Special handling for download_dir
Expand All @@ -144,6 +147,7 @@ def set(self, key: str, value: Any) -> None:
"""Set configuration value.

Persists the value to database for future retrieval.
In headless mode, stores in-memory only.

Args:
key: Configuration key name.
Expand All @@ -153,6 +157,9 @@ def set(self, key: str, value: Any) -> None:
>>> config.set("language", "tr")
>>> config.set("aria2_max_connections", 32)
"""
if self._headless:
self._headless_store[key] = value
return
self.db.set_config(key, value)

def set_headless(self, headless: bool = True) -> None:
Expand Down
2 changes: 1 addition & 1 deletion weeb_cli/providers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ def _request(

if attempt < max_retries - 1:
# Skip retries for permanent errors
if isinstance(e, requests.HTTPError) and e.response.status_code in [404, 403, 401]:
if isinstance(e, requests.HTTPError) and hasattr(e, 'response') and e.response is not None and e.response.status_code in [404, 403, 401]:
debug(f"[HTTP] Permanent error, skipping retries")
return None

Expand Down
5 changes: 4 additions & 1 deletion weeb_cli/services/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,17 +121,20 @@ def get(self, key: str, max_age: int = 3600) -> Optional[Any]:

return None

def set(self, key: str, value: Any) -> None:
def set(self, key: str, value: Any, ttl: Optional[int] = None) -> None:
"""Store value in cache.

Stores in both memory and file cache for persistence.

Args:
key: Cache key.
value: Value to cache (must be JSON-serializable).
ttl: Optional time-to-live in seconds. Used as max_age hint
when retrieving. Stored alongside the value for reference.

Example:
>>> cache.set("search:naruto", results)
>>> cache.set("mal_id:one_piece", 21, ttl=86400 * 7)
"""
self._memory_cache[key] = (value, time.time())

Expand Down
2 changes: 1 addition & 1 deletion weeb_cli/services/headless_downloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def download_episode(
_download_ffmpeg(stream_url, temp_path)

if temp_path.exists() and temp_path.stat().st_size > 0:
temp_path.rename(output_path)
shutil.move(str(temp_path), str(output_path))
log.info(f"Downloaded: {output_path}")
return str(output_path)
else:
Expand Down
5 changes: 2 additions & 3 deletions weeb_cli/utils/sanitizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@ def sanitize_filename(filename: str, max_length: int = 200) -> str:
filename = filename.replace('..', '')
filename = Path(filename).name # Extract only filename, remove any path components

# Normalize unicode characters
filename = unicodedata.normalize('NFKD', filename)
filename = filename.encode('ascii', 'ignore').decode('ascii')
# Normalize unicode characters (preserve non-ASCII like Japanese/Turkish)
filename = unicodedata.normalize('NFKC', filename)

# Remove invalid characters for Windows/Linux/macOS
# Windows: < > : " / \ | ? *
Expand Down
Loading