@@ -21,6 +21,7 @@ import (
2121 virtual_pb "github.com/buildbarn/bb-remote-execution/pkg/proto/configuration/filesystem/virtual"
2222 "github.com/buildbarn/bb-storage/pkg/blockdevice"
2323 "github.com/buildbarn/bb-storage/pkg/clock"
24+ bb_path "github.com/buildbarn/bb-storage/pkg/filesystem/path"
2425 "github.com/buildbarn/bb-storage/pkg/program"
2526 "github.com/buildbarn/bb-storage/pkg/util"
2627 "github.com/stretchr/testify/require"
@@ -39,7 +40,7 @@ func findFreeDriveLetter() (string, error) {
3940 return "" , fmt .Errorf ("no free drive letters available" )
4041}
4142
42- func createWinFSPMountForTest (t * testing.T , terminationGroup program.Group , caseSensitive bool ) (string , blockdevice.BlockDevice ) {
43+ func createWinFSPForTest (t * testing.T , terminationGroup program.Group , caseSensitive bool ) (string , blockdevice.BlockDevice , virtual_configuration. Mount , virtual. Directory ) {
4344 // We can't run winfsp-tests at a directory path due to
4445 // https://github.com/winfsp/winfsp/issues/279. Instead find a free drive
4546 // letter and run it there instead.
@@ -75,38 +76,41 @@ func createWinFSPMountForTest(t *testing.T, terminationGroup program.Group, case
7576
7677 // Create a virtual directory to hold new files.
7778 defaultAttributesSetter := func (requested virtual.AttributesMask , attributes * virtual.Attributes ) {}
78- err = mount .Expose (
79- terminationGroup ,
80- virtual .NewInMemoryPrepopulatedDirectory (
81- virtual .NewHandleAllocatingFileAllocator (
82- virtual .NewPoolBackedFileAllocator (
83- pool .NewBlockDeviceBackedFilePool (
84- bd ,
85- pool .NewBitmapSectorAllocator (uint32 (sectorCount )),
86- sectorSizeBytes ,
87- ),
88- util .DefaultErrorLogger ,
89- defaultAttributesSetter ,
90- virtual .NoNamedAttributesFactory ,
79+ rootDir := virtual .NewInMemoryPrepopulatedDirectory (
80+ virtual .NewHandleAllocatingFileAllocator (
81+ virtual .NewPoolBackedFileAllocator (
82+ pool .NewBlockDeviceBackedFilePool (
83+ bd ,
84+ pool .NewBitmapSectorAllocator (uint32 (sectorCount )),
85+ sectorSizeBytes ,
9186 ),
92- handleAllocator ,
93- ),
94- virtual .NewHandleAllocatingSymlinkFactory (
95- virtual .BaseSymlinkFactory ,
96- handleAllocator .New (),
87+ util .DefaultErrorLogger ,
88+ defaultAttributesSetter ,
89+ virtual .NoNamedAttributesFactory ,
9790 ),
98- util .DefaultErrorLogger ,
9991 handleAllocator ,
100- sort .Sort ,
101- func (s string ) bool { return false },
102- clock .SystemClock ,
103- normalizer ,
104- defaultAttributesSetter ,
105- virtual .NoNamedAttributesFactory ,
10692 ),
93+ virtual .NewHandleAllocatingSymlinkFactory (
94+ virtual .BaseSymlinkFactory ,
95+ handleAllocator .New (),
96+ ),
97+ util .DefaultErrorLogger ,
98+ handleAllocator ,
99+ sort .Sort ,
100+ func (s string ) bool { return false },
101+ clock .SystemClock ,
102+ normalizer ,
103+ defaultAttributesSetter ,
104+ virtual .NoNamedAttributesFactory ,
107105 )
108- require .NoError (t , err , "Failed to expose mount point" )
109106
107+ return vfsPath , bd , mount , rootDir
108+ }
109+
110+ func createWinFSPMountForTest (t * testing.T , terminationGroup program.Group , caseSensitive bool ) (string , blockdevice.BlockDevice ) {
111+ vfsPath , bd , mount , rootDir := createWinFSPForTest (t , terminationGroup , caseSensitive )
112+ err := mount .Expose (terminationGroup , rootDir )
113+ require .NoError (t , err , "Failed to expose mount point" )
110114 return vfsPath , bd
111115}
112116
@@ -417,6 +421,112 @@ func TestWinFSPFileSystemGetSecurityByNameIntegration(t *testing.T) {
417421 })
418422}
419423
424+ func TestWinFSPFileSystemStatFollowsSymlink (t * testing.T ) {
425+ program .RunLocal (context .Background (), func (ctx context.Context , siblingsGroup , dependenciesGroup program.Group ) error {
426+ // Pre-populate the virtual directory through the VFS API
427+ // (not os.Symlink).
428+ vfsPath , bd , mount , rootDir := createWinFSPForTest (t , dependenciesGroup , false )
429+ defer bd .Close ()
430+
431+ // Build a pnpm-style node_modules layout through the VFS
432+ // API with chained symlinks.
433+ var attrs virtual.Attributes
434+
435+ // The real directory.
436+ storeDir , _ , s := rootDir .VirtualMkdir (
437+ bb_path .MustNewComponent ("store" ), 0 , & attrs )
438+ require .Equal (t , virtual .StatusOK , s )
439+ _ , _ , s = storeDir .VirtualMkdir (
440+ bb_path .MustNewComponent ("pkg" ), 0 , & attrs )
441+ require .Equal (t , virtual .StatusOK , s )
442+
443+ // Single symlink: store-link -> store/pkg.
444+ _ , _ , s = rootDir .VirtualSymlink (
445+ ctx ,
446+ []byte ("store/pkg" ),
447+ bb_path .MustNewComponent ("store-link" ),
448+ 0 , & attrs )
449+ require .Equal (t , virtual .StatusOK , s )
450+
451+ // Create node_modules/.pnpm/pkg@1.0.0/node_modules/.
452+ nmDir , _ , s := rootDir .VirtualMkdir (
453+ bb_path .MustNewComponent ("node_modules" ), 0 , & attrs )
454+ require .Equal (t , virtual .StatusOK , s )
455+ pnpmDir , _ , s := nmDir .VirtualMkdir (
456+ bb_path .MustNewComponent (".pnpm" ), 0 , & attrs )
457+ require .Equal (t , virtual .StatusOK , s )
458+ pkgVerDir , _ , s := pnpmDir .VirtualMkdir (
459+ bb_path .MustNewComponent ("pkg@1.0.0" ), 0 , & attrs )
460+ require .Equal (t , virtual .StatusOK , s )
461+ innerNmDir , _ , s := pkgVerDir .VirtualMkdir (
462+ bb_path .MustNewComponent ("node_modules" ), 0 , & attrs )
463+ require .Equal (t , virtual .StatusOK , s )
464+
465+ // Inner symlink.
466+ _ , _ , s = innerNmDir .VirtualSymlink (
467+ ctx ,
468+ []byte ("../../../../store/pkg" ),
469+ bb_path .MustNewComponent ("pkg" ),
470+ 0 , & attrs )
471+ require .Equal (t , virtual .StatusOK , s )
472+
473+ // Outer symlink.
474+ _ , _ , s = nmDir .VirtualSymlink (
475+ ctx ,
476+ []byte (".pnpm/pkg@1.0.0/node_modules/pkg" ),
477+ bb_path .MustNewComponent ("pkg" ),
478+ 0 , & attrs )
479+ require .Equal (t , virtual .StatusOK , s )
480+
481+ require .NoError (t , mount .Expose (dependenciesGroup , rootDir ))
482+
483+ // Write a file into the real directory after mounting.
484+ testContent := []byte (`{"name":"pkg"}` )
485+ require .NoError (t , os .WriteFile (
486+ filepath .Join (vfsPath , "store" , "pkg" , "package.json" ),
487+ testContent , 0o644 ,
488+ ))
489+
490+ t .Run ("SingleSymlink" , func (t * testing.T ) {
491+ singleSymlinkPath := filepath .Join (vfsPath , "store-link" )
492+ info , err := os .Stat (singleSymlinkPath )
493+ require .NoError (t , err )
494+ require .True (t , info .IsDir ())
495+
496+ content , err := os .ReadFile (filepath .Join (singleSymlinkPath , "package.json" ))
497+ require .NoError (t , err )
498+ require .Equal (t , testContent , content )
499+ })
500+
501+ t .Run ("ChainedSymlinks" , func (t * testing.T ) {
502+ symlinkPath := filepath .Join (vfsPath , "node_modules" , "pkg" )
503+
504+ info , err := os .Lstat (symlinkPath )
505+ require .NoError (t , err )
506+ require .NotZero (t , info .Mode ()& os .ModeSymlink )
507+
508+ target , err := os .Readlink (symlinkPath )
509+ require .NoError (t , err )
510+ require .Equal (t , `.pnpm\pkg@1.0.0\node_modules\pkg` , target )
511+
512+ info , err = os .Stat (symlinkPath )
513+ require .NoError (t , err )
514+ require .True (t , info .IsDir ())
515+
516+ content , err := os .ReadFile (filepath .Join (symlinkPath , "package.json" ))
517+ require .NoError (t , err )
518+ require .Equal (t , testContent , content )
519+
520+ entries , err := os .ReadDir (symlinkPath )
521+ require .NoError (t , err )
522+ require .Len (t , entries , 1 )
523+ require .Equal (t , "package.json" , entries [0 ].Name ())
524+ })
525+
526+ return nil
527+ })
528+ }
529+
420530func TestWinFSPFileSystemCasePreserving (t * testing.T ) {
421531 program .RunLocal (context .Background (), func (ctx context.Context , siblingsGroup , dependenciesGroup program.Group ) error {
422532 vfsPath , bd := createWinFSPMountForTest (t , dependenciesGroup , false )
0 commit comments