fix(platform): support Windows storage and native filesystem boundaries

This commit is contained in:
eezd committed 2026-10-27 14:47:25 +08:00
1 parent 3726423338
commit e239fff253
24 files changed
+355 -51

No files matched your search

+5 -4
View File
@@ -13,6 +13,7 @@ import (
"os"
"path"
"path/filepath"
"rawmark/internal/fileutil"
"rawmark/internal/i18n"
"strings"
"syscall"
@@ -42,7 +43,7 @@ type Manifest struct {
type Source struct{ Name, Path string }
func validPath(name string) bool {
return name != "" && path.Clean(name) == name && name != "." && !strings.HasPrefix(name, "/") && name != ".." && !strings.HasPrefix(name, "../") && !strings.ContainsAny(name, "\\\x00")
return fileutil.IsLocalPath(name) && path.Clean(name) == name && name != "." && !strings.ContainsAny(name, "\\\x00")
}
func validEntry(kind, name string) bool {
if !validPath(name) {
@@ -237,11 +238,11 @@ func Extract(ctx context.Context, archive, dest string) (Manifest, error) {
}
expanded += e.Size
}
var disk syscall.Statfs_t
if err := syscall.Statfs(dest, &disk); err != nil {
available, err := fileutil.AvailableBytes(dest)
if err != nil {
return manifest, err
}
if uint64(expanded)+(32<<20) > disk.Bavail*uint64(disk.Bsize) {
if uint64(expanded)+(32<<20) > available {
return manifest, i18n.New("not_enough_disk_space_w_a39b20", syscall.ENOSPC)
}
seen := map[string]bool{}
+32
View File
@@ -0,0 +1,32 @@
package backup
import (
"archive/zip"
"context"
"errors"
"os"
"path/filepath"
"testing"
)
func TestRejectWindowsArchivePaths(t *testing.T) {
for _, name := range []string{"C:escape", "vault/note.md:stream", "vault/CON", "vault/dir./note.md"} {
t.Run(name, func(t *testing.T) {
file := filepath.Join(t.TempDir(), "invalid.zip")
f, err := os.Create(file)
if err != nil {
t.Fatal(err)
}
z := zip.NewWriter(f)
if _, err := z.Create(name); err != nil {
t.Fatal(err)
}
if err := errors.Join(z.Close(), f.Close()); err != nil {
t.Fatal(err)
}
if _, err := Extract(context.Background(), file, t.TempDir()); !errors.Is(err, ErrInvalidArchive) {
t.Fatalf("unsafe archive path accepted: %v", err)
}
})
}
}
+20
View File
@@ -0,0 +1,20 @@
//go:build !windows
package backup
import (
"errors"
"os"
"path/filepath"
)
func publishRecord(source, dest string) error {
if err := os.Rename(source, dest); err != nil {
return err
}
dir, err := os.Open(filepath.Dir(dest))
if err != nil {
return err
}
return errors.Join(dir.Sync(), dir.Close())
}
+16
View File
@@ -0,0 +1,16 @@
package backup
import "golang.org/x/sys/windows"
func publishRecord(source, dest string) error {
from, err := windows.UTF16PtrFromString(source)
if err != nil {
return err
}
to, err := windows.UTF16PtrFromString(dest)
if err != nil {
return err
}
// The file was flushed before closing; request a completed on-disk replacement.
return windows.MoveFileEx(from, to, windows.MOVEFILE_REPLACE_EXISTING|windows.MOVEFILE_WRITE_THROUGH)
}
+1 -9
View File
@@ -51,15 +51,7 @@ func WriteRecord(data string, record RestoreRecord) error {
if err != nil {
return err
}
if err = os.Rename(f.Name(), dest); err != nil {
return err
}
dir, err := os.Open(filepath.Dir(dest))
if err != nil {
return err
}
defer dir.Close()
return dir.Sync()
return publishRecord(f.Name(), dest)
}
func Pending(data string) (bool, error) {
_, err := os.Stat(recordPath(data))