@@ -2,9 +2,9 @@ import { execFileSync } from 'node:child_process';
22import { fixtureGit } from './fixtures/git.ts' ;
33import { mkdtempSync , mkdirSync , readFileSync , readdirSync , rmSync , lstatSync , symlinkSync , writeFileSync , renameSync , opendirSync } from 'node:fs' ;
44import { tmpdir } from 'node:os' ;
5- import { join } from 'node:path' ;
5+ import { join , win32 } from 'node:path' ;
66import { afterEach , describe , expect , it , vi } from 'vitest' ;
7- import { createTaskClone } from '../git/clone.ts' ;
7+ import { createTaskClone , pathIsWithin } from '../git/clone.ts' ;
88
99vi . mock ( 'node:fs' , async importOriginal => {
1010 const actual = await importOriginal < typeof import ( 'node:fs' ) > ( ) ;
@@ -26,6 +26,22 @@ function fixture() {
2626 return { source, parent, head : git ( source , 'rev-parse' , 'HEAD' ) , taskId : 'task-1' } ;
2727}
2828afterEach ( ( ) => { vi . restoreAllMocks ( ) ; vi . resetAllMocks ( ) ; for ( const root of roots . splice ( 0 ) ) rmSync ( root , { recursive : true , force : true } ) ; } ) ;
29+ describe ( 'path containment' , ( ) => {
30+ it . each ( [
31+ [ 'same directory despite case' , 'C:\\repo' , 'c:\\REPO' , true ] ,
32+ [ 'nested directory' , 'C:\\repo' , 'C:\\repo\\tasks' , true ] ,
33+ [ 'similarly named sibling' , 'C:\\repo' , 'C:\\repository' , false ] ,
34+ [ 'same-drive sibling' , 'C:\\repo' , 'C:\\tasks' , false ] ,
35+ [ 'parent directory' , 'C:\\repo\\source' , 'C:\\repo' , false ] ,
36+ [ 'different drive' , 'C:\\repo' , 'D:\\tasks' , false ] ,
37+ [ 'nested UNC directory' , '\\\\server\\share\\repo' , '\\\\server\\share\\repo\\tasks' , true ] ,
38+ [ 'same-share UNC sibling' , '\\\\server\\share\\repo' , '\\\\server\\share\\tasks' , false ] ,
39+ [ 'different UNC share' , '\\\\server\\share\\repo' , '\\\\server\\other\\tasks' , false ] ,
40+ [ 'different UNC server' , '\\\\server\\share\\repo' , '\\\\other\\share\\tasks' , false ] ,
41+ ] ) ( 'uses Windows boundaries for a %s' , ( _name , base , candidate , expected ) => {
42+ expect ( pathIsWithin ( base , candidate , win32 ) ) . toBe ( expected ) ;
43+ } ) ;
44+ } ) ;
2945describe ( 'isolated staging clone' , ( ) => {
3046 it ( 'copies objects, ignores dirty source changes, and has no origin or shared metadata' , ( ) => {
3147 const input = fixture ( ) ;
@@ -66,9 +82,9 @@ describe('isolated staging clone', () => {
6682 git ( input . source , 'update-ref' , `refs/replace/${ input . head } ` , input . head ) ;
6783 expect ( ( ) => createTaskClone ( input ) ) . toThrow ( 'Replacement' ) ;
6884 } ) ;
69- it ( 'rejects nested storage, including paths through symlinks ' , ( ) => {
85+ it ( 'rejects nested storage after resolving a directory alias , including a Windows junction ' , ( ) => {
7086 const input = fixture ( ) , nested = join ( input . source , 'tasks' ) , alias = join ( input . parent , 'alias' ) ;
71- mkdirSync ( nested ) ; symlinkSync ( nested , alias ) ;
87+ mkdirSync ( nested ) ; symlinkSync ( nested , alias , process . platform === 'win32' ? 'junction' : 'dir' ) ;
7288 expect ( ( ) => createTaskClone ( { ...input , parent : alias } ) ) . toThrow ( 'outside' ) ;
7389 } ) ;
7490 it ( 'requires a full existing commit and finite time budget' , ( ) => {
0 commit comments