2727package cowfs
2828
2929import (
30+ "context"
3031 "crypto/sha1"
3132 "errors"
3233 "fmt"
@@ -241,16 +242,17 @@ func (u *FS) resolvePath(path string) (string, error) {
241242// shouldCopy determines if a file needs to be copied from base to overlay.
242243// Returns true if the file exists in base but not in overlay.
243244// Returns false if already in overlay or doesn't exist in base.
245+ // Uses Lstat to properly handle symlinks without following them.
244246func (u * FS ) shouldCopy (name string ) (bool , error ) {
245- // Already in overlay?
246- if _ , err := fs .Stat (u .Overlay , name ); err == nil {
247+ // Already in overlay? (use Lstat to not follow symlinks)
248+ if _ , err := fs .Lstat (u .Overlay , name ); err == nil {
247249 return false , nil
248250 } else if ! errors .Is (err , fs .ErrNotExist ) {
249251 return false , err
250252 }
251253
252- // Exists in base?
253- if _ , err := fs .Stat (u .Base , name ); err == nil {
254+ // Exists in base? (use Lstat to not follow symlinks)
255+ if _ , err := fs .Lstat (u .Base , name ); err == nil {
254256 return true , nil
255257 } else if errors .Is (err , fs .ErrNotExist ) {
256258 return false , fs .ErrNotExist
@@ -348,16 +350,16 @@ func (u *FS) Rename(oldname, newname string) error {
348350 }
349351 // Note: Do NOT resolve newname through renames - POSIX rename overwrites newname itself
350352
351- // 2. Check if source exists in base and overlay
353+ // 2. Check if source exists in base and overlay (use Lstat to not follow symlinks)
352354 srcInBase := false
353- if _ , err := fs .Stat (u .Base , src ); err == nil {
355+ if _ , err := fs .Lstat (u .Base , src ); err == nil {
354356 srcInBase = true
355357 } else if ! errors .Is (err , fs .ErrNotExist ) {
356358 return err
357359 }
358360
359361 srcInOverlay := false
360- if _ , err := fs .Stat (u .Overlay , src ); err == nil {
362+ if _ , err := fs .Lstat (u .Overlay , src ); err == nil {
361363 srcInOverlay = true
362364 } else if ! errors .Is (err , fs .ErrNotExist ) {
363365 return err
@@ -369,7 +371,8 @@ func (u *FS) Rename(oldname, newname string) error {
369371 }
370372
371373 // 4. Handle existing destination: remove if in overlay, tombstone if in base
372- statInfo , overlayErr := fs .Stat (u .Overlay , newname )
374+ // Use Lstat to not follow symlinks
375+ statInfo , overlayErr := fs .Lstat (u .Overlay , newname )
373376 if overlayErr == nil {
374377 // Destination exists in overlay - remove it
375378 if statInfo .IsDir () {
@@ -386,7 +389,7 @@ func (u *FS) Rename(oldname, newname string) error {
386389 return overlayErr
387390 }
388391
389- _ , baseErr := fs .Stat (u .Base , newname )
392+ _ , baseErr := fs .Lstat (u .Base , newname )
390393 if baseErr == nil {
391394 // Destination exists in base - tombstone it
392395 if err := u .tombstone (newname ); err != nil {
@@ -483,17 +486,19 @@ func (u *FS) Remove(name string) error {
483486 }
484487
485488 // 2. Check if file exists in base (we'll need this info)
489+ // Use Lstat to not follow symlinks
486490 existsInBase := false
487- if _ , err := fs .Stat (u .Base , target ); err == nil {
491+ if _ , err := fs .Lstat (u .Base , target ); err == nil {
488492 existsInBase = true
489493 } else if ! errors .Is (err , fs .ErrNotExist ) {
490494 log .Println ("stat base error" , err )
491495 return err
492496 }
493497
494498 // 3. Check if file exists in overlay
499+ // Use Lstat to not follow symlinks
495500 existsInOverlay := false
496- if _ , err := fs .Stat (u .Overlay , target ); err == nil {
501+ if _ , err := fs .Lstat (u .Overlay , target ); err == nil {
497502 existsInOverlay = true
498503 } else if ! errors .Is (err , fs .ErrNotExist ) {
499504 log .Println ("stat overlay error" , err )
@@ -520,14 +525,14 @@ func (u *FS) Remove(name string) error {
520525 return fs .ErrNotExist
521526 }
522527
523- // 6. Check if it's a directory
528+ // 6. Check if it's a directory (use Lstat to not follow symlinks)
524529 isDir := false
525530 if existsInOverlay {
526- if info , err := fs .Stat (u .Overlay , target ); err == nil {
531+ if info , err := fs .Lstat (u .Overlay , target ); err == nil {
527532 isDir = info .IsDir ()
528533 }
529534 } else if existsInBase {
530- if info , err := fs .Stat (u .Base , target ); err == nil {
535+ if info , err := fs .Lstat (u .Base , target ); err == nil {
531536 isDir = info .IsDir ()
532537 }
533538 }
@@ -661,15 +666,16 @@ func (u *FS) Symlink(oldname, newname string) error {
661666 }
662667
663668 // 3. Handle existing file at target path (remove overlay, tombstone base)
664- if _ , err := fs .Stat (u .Overlay , newpath ); err == nil {
669+ // Use Lstat to not follow symlinks
670+ if _ , err := fs .Lstat (u .Overlay , newpath ); err == nil {
665671 if err := fs .Remove (u .Overlay , newpath ); err != nil {
666672 return err
667673 }
668674 } else if ! errors .Is (err , fs .ErrNotExist ) {
669675 return err
670676 }
671677
672- if _ , err := fs .Stat (u .Base , newpath ); err == nil {
678+ if _ , err := fs .Lstat (u .Base , newpath ); err == nil {
673679 // Hide base entry
674680 if err := u .tombstone (newpath ); err != nil {
675681 return err
@@ -705,14 +711,15 @@ func (u *FS) Mkdir(name string, perm os.FileMode) error {
705711 // log.Println("Mkdir", name, perm)
706712
707713 // 2. Check if directory already exists in either layer
708- if _ , err := fs .Stat (u .Overlay , path ); err == nil {
714+ // Use Lstat to not follow symlinks
715+ if _ , err := fs .Lstat (u .Overlay , path ); err == nil {
709716 return fs .ErrExist
710717 } else if ! errors .Is (err , fs .ErrNotExist ) {
711718 return err
712719 }
713720
714721 existsInBase := false
715- if _ , err := fs .Stat (u .Base , path ); err == nil {
722+ if _ , err := fs .Lstat (u .Base , path ); err == nil {
716723 existsInBase = true
717724 } else if ! errors .Is (err , fs .ErrNotExist ) {
718725 return err
@@ -779,9 +786,17 @@ func (u *FS) Create(name string) (fs.File, error) {
779786// The path is resolved through rename chains before processing.
780787// Prefers overlay, falls back to base. Returns fs.ErrNotExist if tombstoned.
781788func (u * FS ) Stat (name string ) (os.FileInfo , error ) {
789+ return u .StatContext (context .Background (), name )
790+ }
791+
792+ // StatContext returns file information for the named file with context support.
793+ // Respects the NoFollow context flag for symlink handling.
794+ // The path is resolved through rename chains before processing.
795+ // Prefers overlay, falls back to base. Returns fs.ErrNotExist if tombstoned.
796+ func (u * FS ) StatContext (ctx context.Context , name string ) (os.FileInfo , error ) {
782797 // 0. Normalize path
783798 name = filepath .Clean (name )
784- // log.Println("Stat ", name)
799+ // log.Println("StatContext ", name, "follow:", fs.FollowSymlinks(ctx) )
785800
786801 // 1. Resolve rename chain
787802 path , err := u .resolvePath (name )
@@ -794,15 +809,21 @@ func (u *FS) Stat(name string) (os.FileInfo, error) {
794809 return nil , fs .ErrNotExist
795810 }
796811
812+ // Choose stat function based on whether we should follow symlinks
813+ statFn := fs .Stat
814+ if ! fs .FollowSymlinks (ctx ) {
815+ statFn = fs .Lstat
816+ }
817+
797818 // 3. Try overlay first
798- if fi , err := fs . Stat (u .Overlay , path ); err == nil {
819+ if fi , err := statFn (u .Overlay , path ); err == nil {
799820 return fi , nil
800821 } else if ! errors .Is (err , fs .ErrNotExist ) {
801822 return nil , err
802823 }
803824
804825 // 4. Fallback to base
805- fi , err := fs . Stat (u .Base , path )
826+ fi , err := statFn (u .Base , path )
806827 if err != nil {
807828 return nil , err
808829 }
@@ -901,16 +922,16 @@ func (u *FS) OpenFile(name string, flag int, perm os.FileMode) (fs.File, error)
901922 }
902923 }
903924
904- // 2. Check existence in both layers
925+ // 2. Check existence in both layers (use Lstat to not follow symlinks)
905926 existsInOverlay := false
906- if _ , err := fs .Stat (u .Overlay , path ); err == nil {
927+ if _ , err := fs .Lstat (u .Overlay , path ); err == nil {
907928 existsInOverlay = true
908929 } else if ! errors .Is (err , fs .ErrNotExist ) {
909930 return nil , err
910931 }
911932
912933 existsInBase := false
913- if _ , err := fs .Stat (u .Base , path ); err == nil {
934+ if _ , err := fs .Lstat (u .Base , path ); err == nil {
914935 existsInBase = true
915936 } else if ! errors .Is (err , fs .ErrNotExist ) {
916937 return nil , err
@@ -921,7 +942,7 @@ func (u *FS) OpenFile(name string, flag int, perm os.FileMode) (fs.File, error)
921942 exclusive := flag & os .O_EXCL != 0
922943 if creating && exclusive {
923944 baseExists := false
924- if _ , err := fs .Stat (u .Base , path ); err == nil {
945+ if _ , err := fs .Lstat (u .Base , path ); err == nil {
925946 if _ , dead := u .tombstones .Load (path ); ! dead {
926947 baseExists = true
927948 }
0 commit comments