@@ -121,13 +121,7 @@ public async Task Should_Reject_SetProvider_When_Sessions_Already_Exist()
121121 }
122122 finally
123123 {
124- try
125- {
126- await client2 . ForceStopAsync ( ) ;
127- }
128- catch
129- {
130- }
124+ await client2 . ForceStopAsync ( ) ;
131125 }
132126 }
133127 finally
@@ -255,15 +249,18 @@ private CopilotClient CreateSessionFsClient(string providerRoot, bool useStdio =
255249 }
256250
257251 private static string CreateProviderRoot ( )
258- => Path . Combine ( Path . GetTempPath ( ) , $ "copilot-sessionfs-{ Guid . NewGuid ( ) : N} ") ;
252+ => Path . Join ( Path . GetTempPath ( ) , $ "copilot-sessionfs-{ Guid . NewGuid ( ) : N} ") ;
259253
260254 private static string GetStoredPath ( string providerRoot , string sessionId , string sessionPath )
261255 {
256+ var safeSessionId = NormalizeRelativePathSegment ( sessionId , nameof ( sessionId ) ) ;
262257 var relativeSegments = sessionPath
263258 . TrimStart ( '/' , '\\ ' )
264- . Split ( [ '/' , '\\ ' ] , StringSplitOptions . RemoveEmptyEntries ) ;
259+ . Split ( [ '/' , '\\ ' ] , StringSplitOptions . RemoveEmptyEntries )
260+ . Select ( segment => NormalizeRelativePathSegment ( segment , nameof ( sessionPath ) ) )
261+ . ToArray ( ) ;
265262
266- return Path . Combine ( [ providerRoot , sessionId , .. relativeSegments ] ) ;
263+ return Path . Join ( [ providerRoot , safeSessionId , .. relativeSegments ] ) ;
267264 }
268265
269266 private static async Task WaitForConditionAsync ( Func < bool > condition , TimeSpan ? timeout = null )
@@ -274,6 +271,7 @@ private static async Task WaitForConditionAsync(Func<bool> condition, TimeSpan?
274271 private static async Task WaitForConditionAsync ( Func < Task < bool > > condition , TimeSpan ? timeout = null )
275272 {
276273 var deadline = DateTime . UtcNow + ( timeout ?? TimeSpan . FromSeconds ( 30 ) ) ;
274+ Exception ? lastException = null ;
277275 while ( DateTime . UtcNow < deadline )
278276 {
279277 try
@@ -283,17 +281,19 @@ private static async Task WaitForConditionAsync(Func<Task<bool>> condition, Time
283281 return ;
284282 }
285283 }
286- catch ( IOException )
284+ catch ( IOException ex )
287285 {
286+ lastException = ex ;
288287 }
289- catch ( UnauthorizedAccessException )
288+ catch ( UnauthorizedAccessException ex )
290289 {
290+ lastException = ex ;
291291 }
292292
293293 await Task . Delay ( 100 ) ;
294294 }
295295
296- throw new TimeoutException ( "Timed out waiting for condition." ) ;
296+ throw new TimeoutException ( "Timed out waiting for condition." , lastException ) ;
297297 }
298298
299299 private static async Task < string > ReadAllTextSharedAsync ( string path , CancellationToken cancellationToken = default )
@@ -305,16 +305,26 @@ private static async Task<string> ReadAllTextSharedAsync(string path, Cancellati
305305
306306 private static void TryDeleteDirectory ( string path )
307307 {
308- try
308+ if ( Directory . Exists ( path ) )
309309 {
310- if ( Directory . Exists ( path ) )
311- {
312- Directory . Delete ( path , recursive : true ) ;
313- }
310+ Directory . Delete ( path , recursive : true ) ;
314311 }
315- catch
312+ }
313+
314+ private static string NormalizeRelativePathSegment ( string segment , string paramName )
315+ {
316+ if ( string . IsNullOrWhiteSpace ( segment ) )
316317 {
318+ throw new InvalidOperationException ( $ "{ paramName } must not be empty.") ;
317319 }
320+
321+ var normalized = segment . TrimStart ( Path . DirectorySeparatorChar , Path . AltDirectorySeparatorChar ) ;
322+ if ( Path . IsPathRooted ( normalized ) || normalized . Contains ( Path . VolumeSeparatorChar ) )
323+ {
324+ throw new InvalidOperationException ( $ "{ paramName } must be a relative path segment: { segment } ") ;
325+ }
326+
327+ return normalized ;
318328 }
319329
320330 private sealed class TestSessionFsHandler ( string sessionId , string rootDir ) : ISessionFsHandler
@@ -456,12 +466,15 @@ public Task RenameAsync(SessionFsRenameParams request, CancellationToken cancell
456466
457467 private string ResolvePath ( string sessionPath )
458468 {
459- var sessionRoot = Path . GetFullPath ( Path . Combine ( rootDir , sessionId ) ) ;
469+ var normalizedSessionId = NormalizeRelativePathSegment ( sessionId , nameof ( sessionId ) ) ;
470+ var sessionRoot = Path . GetFullPath ( Path . Join ( rootDir , normalizedSessionId ) ) ;
460471 var relativeSegments = sessionPath
461472 . TrimStart ( '/' , '\\ ' )
462- . Split ( [ '/' , '\\ ' ] , StringSplitOptions . RemoveEmptyEntries ) ;
473+ . Split ( [ '/' , '\\ ' ] , StringSplitOptions . RemoveEmptyEntries )
474+ . Select ( segment => NormalizeRelativePathSegment ( segment , nameof ( sessionPath ) ) )
475+ . ToArray ( ) ;
463476
464- var fullPath = Path . GetFullPath ( Path . Combine ( [ sessionRoot , .. relativeSegments ] ) ) ;
477+ var fullPath = Path . GetFullPath ( Path . Join ( [ sessionRoot , .. relativeSegments ] ) ) ;
465478 if ( ! fullPath . StartsWith ( sessionRoot , StringComparison . Ordinal ) )
466479 {
467480 throw new InvalidOperationException ( $ "Path escapes session root: { sessionPath } ") ;
0 commit comments