88from pathlib import Path
99
1010from minicode .tooling import ToolDefinition , ToolContext , ToolResult
11+ from minicode .workspace import resolve_tool_path
12+
13+
14+ def _resolve_archive_member (destination : Path , member_name : str ) -> Path :
15+ normalized_name = member_name .replace ("\\ " , "/" )
16+ member_path = Path (normalized_name )
17+ if member_path .is_absolute () or any (part == ".." for part in member_path .parts ):
18+ raise ValueError (f"Archive member escapes extraction destination: { member_name } " )
19+
20+ destination_root = destination .resolve ()
21+ target = (destination_root / member_path ).resolve ()
22+ try :
23+ target .relative_to (destination_root )
24+ except ValueError as error :
25+ raise ValueError (f"Archive member escapes extraction destination: { member_name } " ) from error
26+ return target
27+
28+
29+ def _safe_extract_zip (source : Path , destination : Path ) -> None :
30+ destination .mkdir (parents = True , exist_ok = True )
31+ with zipfile .ZipFile (source , "r" ) as zf :
32+ for info in zf .infolist ():
33+ target = _resolve_archive_member (destination , info .filename )
34+ if info .is_dir ():
35+ target .mkdir (parents = True , exist_ok = True )
36+ continue
37+ target .parent .mkdir (parents = True , exist_ok = True )
38+ with zf .open (info , "r" ) as src , open (target , "wb" ) as dst :
39+ shutil .copyfileobj (src , dst )
40+
41+
42+ def _safe_extract_tar (source : Path , destination : Path ) -> None :
43+ destination .mkdir (parents = True , exist_ok = True )
44+ with tarfile .open (source , "r:*" ) as tar :
45+ for member in tar .getmembers ():
46+ if member .issym () or member .islnk ():
47+ raise ValueError (f"Archive member uses unsupported link: { member .name } " )
48+ target = _resolve_archive_member (destination , member .name )
49+ if member .isdir ():
50+ target .mkdir (parents = True , exist_ok = True )
51+ continue
52+ if not member .isfile ():
53+ continue
54+ target .parent .mkdir (parents = True , exist_ok = True )
55+ src = tar .extractfile (member )
56+ if src is None :
57+ continue
58+ with src , open (target , "wb" ) as dst :
59+ shutil .copyfileobj (src , dst )
1160
1261
1362# ---------------------------------------------------------------------------
@@ -185,23 +234,20 @@ def _validate_tar_extract(input_data: dict) -> dict:
185234
186235
187236def _run_tar_extract (input_data : dict , context : ToolContext ) -> ToolResult :
188- source = Path (context . cwd ) / input_data ["source" ]
237+ source = resolve_tool_path (context , input_data ["source" ], "read" )
189238 dest_dir = input_data .get ("destination" , "" )
190239
191240 if not source .exists ():
192241 return ToolResult (ok = False , output = f"Source not found: { input_data ['source' ]} " )
193242
194243 try :
195244 if dest_dir :
196- destination = Path (context . cwd ) / dest_dir
245+ destination = resolve_tool_path (context , dest_dir , "write" )
197246 else :
198247 # Extract to same directory as archive
199248 destination = source .parent / source .stem
200249
201- destination .mkdir (parents = True , exist_ok = True )
202-
203- with tarfile .open (source , "r:*" ) as tar :
204- tar .extractall (destination )
250+ _safe_extract_tar (source , destination )
205251
206252 return ToolResult (ok = True , output = f"Extracted to { destination } " )
207253 except Exception as e :
@@ -291,22 +337,19 @@ def _validate_zip_extract(input_data: dict) -> dict:
291337
292338
293339def _run_zip_extract (input_data : dict , context : ToolContext ) -> ToolResult :
294- source = Path (context . cwd ) / input_data ["source" ]
340+ source = resolve_tool_path (context , input_data ["source" ], "read" )
295341 dest_dir = input_data .get ("destination" , "" )
296342
297343 if not source .exists ():
298344 return ToolResult (ok = False , output = f"Source not found: { input_data ['source' ]} " )
299345
300346 try :
301347 if dest_dir :
302- destination = Path (context . cwd ) / dest_dir
348+ destination = resolve_tool_path (context , dest_dir , "write" )
303349 else :
304350 destination = source .parent / source .stem
305351
306- destination .mkdir (parents = True , exist_ok = True )
307-
308- with zipfile .ZipFile (source , "r" ) as zf :
309- zf .extractall (destination )
352+ _safe_extract_zip (source , destination )
310353
311354 return ToolResult (ok = True , output = f"Extracted to { destination } " )
312355 except Exception as e :
@@ -326,4 +369,4 @@ def _run_zip_extract(input_data: dict, context: ToolContext) -> ToolResult:
326369 },
327370 validator = _validate_zip_extract ,
328371 run = _run_zip_extract ,
329- )
372+ )
0 commit comments