Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions osfs/os_chroot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"testing"

"github.com/go-git/go-billy/v6"
"github.com/go-git/go-billy/v6/util"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -68,3 +69,48 @@ func TestCreateWithChroot(t *testing.T) {
t, expected, actual, "Permission mismatch - expected: 0o%o, actual: 0o%o", expected, actual,
)
}

// Verify that the Rename() is successful even if the destination is a
// read-only file.
func TestRenameToReadonly(t *testing.T) {
fs, _ := setup(t)
chroot, _ := fs.Chroot("rename")

// Prepare two files: rename source and destination
err := util.WriteFile(chroot, "src.txt", []byte("hello"), 0o644)
if err != nil {
t.Fatalf("failed to write src.txt: %s", err)
}
err = util.WriteFile(chroot, "dst.txt", []byte("world"), 0o444)
if err != nil {
t.Fatalf("failed to write dst.txt: %s", err)
}

err = chroot.Rename("src.txt", "dst.txt")
if err != nil {
t.Fatalf("failed to rename to overwrite read-only file: %s", err)
}

// src.txt must not exist
_, err = chroot.Stat("src.txt")
if err == nil {
t.Error("src.txt must not exist, but does it")
} else if !os.IsNotExist(err) {
t.Errorf("unexpected error on src.txt: %s", err)
}

// Check dst.txt's permission and contents.
fi, err := chroot.Stat("dst.txt")
if err != nil {
t.Errorf("unexpected error on dst.txt: %s", err)
}
if perm := fi.Mode().Perm(); perm != 0o444 {
t.Errorf("unexpected permission of dst.txt: %04o", perm)
}
b, err := util.ReadFile(chroot, "dst.txt")
if err != nil {
t.Errorf("failed to read dst.txt: %s", err)
} else if string(b) != "hello" {
t.Errorf("unexpected contents of dst.txt: want=%q got=%q", "hello", string(b))
}
}
31 changes: 30 additions & 1 deletion osfs/os_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package osfs

import (
"io/fs"
"os"
"runtime"
"unsafe"
Expand Down Expand Up @@ -52,7 +53,35 @@ func (f *file) Sync() error {
}

func rename(from, to string) error {
return os.Rename(from, to)
// On Windows, os.Rename() fails when a read-only file is specified as the
// destination. (On Linux, for example, it succeeds even if the file is
// read-only if you have write permission in the parent directory.)
// Therefore, for read-only files, we must first change the permissions to
// allow writing, then rename them with os.Rename(), and then restore their
// original permissions.
var (
modeChanged bool
originalMode fs.FileMode
)
if fi, err := os.Stat(to); err == nil {
originalMode = fi.Mode()
if originalMode&0o200 == 0 {
err := os.Chmod(to, originalMode|0o200)
if err != nil {
return err
}
modeChanged = true
}
}
err := os.Rename(from, to)
if err != nil {
return err
}
// If we changed permissions, change them back
if modeChanged {
return os.Chmod(to, originalMode)
}
return nil
}

func umask(_ int) func() {
Expand Down