Skip to content

Commit 35994ce

Browse files
committed
Gdrive file type fileter - Fixing problems detected by coderabbitai
1 parent a6c0d9d commit 35994ce

3 files changed

Lines changed: 71 additions & 14 deletions

File tree

gdrive/drive_helpers.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,9 @@ def build_drive_list_params(
218218
SHORTCUT_MIME_TYPE = "application/vnd.google-apps.shortcut"
219219
FOLDER_MIME_TYPE = "application/vnd.google-apps.folder"
220220

221+
# RFC 6838 token-style MIME type validation (safe for Drive query interpolation).
222+
MIME_TYPE_PATTERN = re.compile(r"^[A-Za-z0-9!#$&^_.+-]+/[A-Za-z0-9!#$&^_.+-]+$")
223+
221224
# Mapping from friendly type names to Google Drive MIME types.
222225
# Raw MIME type strings (containing '/') are always accepted as-is.
223226
FILE_TYPE_MIME_MAP: Dict[str, str] = {
@@ -256,9 +259,14 @@ def resolve_file_type_mime(file_type: str) -> str:
256259
Raises:
257260
ValueError: If the value is not a recognised friendly name and contains no '/'.
258261
"""
259-
if "/" in file_type:
260-
return file_type
261-
lower = file_type.lower()
262+
normalized = file_type.strip()
263+
if "/" in normalized:
264+
if not MIME_TYPE_PATTERN.fullmatch(normalized):
265+
raise ValueError(
266+
f"Invalid MIME type '{file_type}'. Expected format like 'application/pdf'."
267+
)
268+
return normalized
269+
lower = normalized.lower()
262270
if lower not in FILE_TYPE_MIME_MAP:
263271
valid = ", ".join(sorted(FILE_TYPE_MIME_MAP.keys()))
264272
raise ValueError(

gdrive/drive_tools.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,10 @@ async def search_drive_files(
7676
If 'drive_id' is specified and 'corpora' is None, it defaults to 'drive'.
7777
Otherwise, Drive API default behavior applies. Prefer 'user' or 'drive' over 'allDrives' for efficiency.
7878
file_type (Optional[str]): Restrict results to a specific file type. Accepts a friendly
79-
name ('folder', 'document', 'doc', 'spreadsheet', 'sheet', 'presentation', 'slides', 'form', 'drawing', 'pdf', 'shortcut') or
80-
any raw MIME type string (e.g. 'application/pdf'). Defaults to None (all types).
79+
name ('folder', 'document'/'doc', 'spreadsheet'/'sheet',
80+
'presentation'/'slides', 'form', 'drawing', 'pdf', 'shortcut',
81+
'script', 'site', 'jam'/'jamboard') or any raw MIME type
82+
string (e.g. 'application/pdf'). Defaults to None (all types).
8183
8284
Returns:
8385
str: A formatted list of found files/folders with their details (ID, name, type, size, modified time, link).
@@ -105,7 +107,7 @@ async def search_drive_files(
105107

106108
if file_type is not None:
107109
mime = resolve_file_type_mime(file_type)
108-
final_query += f" and mimeType = '{mime}'"
110+
final_query = f"({final_query}) and mimeType = '{mime}'"
109111
logger.info(f"[search_drive_files] Added mimeType filter: '{mime}'")
110112

111113
list_params = build_drive_list_params(
@@ -439,10 +441,10 @@ async def list_drive_items(
439441
include_items_from_all_drives (bool): Whether items from all accessible shared drives should be included if `drive_id` is not set. Defaults to True.
440442
corpora (Optional[str]): Corpus to query ('user', 'drive', 'allDrives'). If `drive_id` is set and `corpora` is None, 'drive' is used. If None and no `drive_id`, API defaults apply.
441443
file_type (Optional[str]): Restrict results to a specific file type. Accepts a friendly
442-
name ('folder', 'document', 'doc', 'spreadsheet', 'sheet',
443-
'presentation', 'slides', 'form', 'drawing', 'pdf',
444-
'shortcut') or any raw MIME type string
445-
(e.g. 'application/pdf'). Defaults to None (all types).
444+
name ('folder', 'document'/'doc', 'spreadsheet'/'sheet',
445+
'presentation'/'slides', 'form', 'drawing', 'pdf', 'shortcut',
446+
'script', 'site', 'jam'/'jamboard') or any raw MIME type
447+
string (e.g. 'application/pdf'). Defaults to None (all types).
446448
447449
Returns:
448450
str: A formatted list of files/folders in the specified folder.
@@ -456,7 +458,7 @@ async def list_drive_items(
456458

457459
if file_type is not None:
458460
mime = resolve_file_type_mime(file_type)
459-
final_query += f" and mimeType = '{mime}'"
461+
final_query = f"({final_query}) and mimeType = '{mime}'"
460462
logger.info(f"[list_drive_items] Added mimeType filter: '{mime}'")
461463

462464
list_params = build_drive_list_params(

tests/gdrive/test_drive_tools.py

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,10 @@ async def test_create_drive_folder():
5555
# Helpers
5656
# ---------------------------------------------------------------------------
5757

58-
def _unwrap(tool):
59-
"""Unwrap a FunctionTool + decorator chain to the original async function."""
60-
fn = tool.fn
58+
def _unwrap(fn):
59+
"""Unwrap a FunctionTool or plain-function decorator chain to the original async function."""
60+
if hasattr(fn, "fn"):
61+
fn = fn.fn # FunctionTool wrapper (some server versions)
6162
while hasattr(fn, "__wrapped__"):
6263
fn = fn.__wrapped__
6364
return fn
@@ -396,3 +397,49 @@ async def test_list_items_file_type_unknown_raises(mock_resolve_folder):
396397
folder_id="root",
397398
file_type="unknowntype",
398399
)
400+
401+
402+
# ---------------------------------------------------------------------------
403+
# OR-precedence grouping
404+
# ---------------------------------------------------------------------------
405+
406+
407+
@pytest.mark.asyncio
408+
async def test_search_or_query_is_grouped_before_mime_filter():
409+
"""An OR structured query is wrapped in parentheses so the MIME filter binds correctly."""
410+
mock_service = Mock()
411+
mock_service.files().list().execute.return_value = {"files": []}
412+
413+
await _unwrap(search_drive_files)(
414+
service=mock_service,
415+
user_google_email="user@example.com",
416+
query="name contains 'a' or name contains 'b'",
417+
file_type="document",
418+
)
419+
420+
q = mock_service.files.return_value.list.call_args.kwargs["q"]
421+
# Without grouping this would be: name contains 'a' or name contains 'b' and mimeType = ...
422+
# The 'and' would only bind to the second term, leaking the first term through unfiltered.
423+
assert q.startswith("(")
424+
assert "name contains 'a' or name contains 'b'" in q
425+
assert ") and mimeType = 'application/vnd.google-apps.document'" in q
426+
427+
428+
# ---------------------------------------------------------------------------
429+
# MIME type validation
430+
# ---------------------------------------------------------------------------
431+
432+
433+
def test_resolve_file_type_mime_invalid_mime_raises():
434+
"""A raw string with '/' but containing quotes raises ValueError."""
435+
from gdrive.drive_helpers import resolve_file_type_mime
436+
437+
with pytest.raises(ValueError, match="Invalid MIME type"):
438+
resolve_file_type_mime("application/pdf' or '1'='1")
439+
440+
441+
def test_resolve_file_type_mime_strips_whitespace():
442+
"""Leading/trailing whitespace is stripped from raw MIME strings."""
443+
from gdrive.drive_helpers import resolve_file_type_mime
444+
445+
assert resolve_file_type_mime(" application/pdf ") == "application/pdf"

0 commit comments

Comments
 (0)