返回 DeepSeek-Reasonix
native_windows_test.go
根目录 / internal / fileops / native_windows_test.go
1 //go:build windows
2
3 package fileops
4
5 import (
6 "os"
7 "os/exec"
8 "path/filepath"
9 "strings"
10 "testing"
11 "unsafe"
12
13 "golang.org/x/sys/windows"
14 )
15
16 func TestWindowsSnapshotUsesVolumeFileIndexAndChangeTime(t *testing.T) {
17 path := filepath.Join(t.TempDir(), "version.txt")
18 if err := os.WriteFile(path, []byte("one"), 0o600); err != nil {
19 t.Fatal(err)
20 }
21 info, err := os.Stat(path)
22 if err != nil {
23 t.Fatal(err)
24 }
25 target, before := DiskSnapshot(path, info)
26 if !strings.Contains(target.Key, "volume=") || !strings.Contains(target.Key, "fileindex=") {
27 t.Fatalf("target lacks native identity: %q", target.Key)
28 }
29 name, err := windows.UTF16PtrFromString(path)
30 if err != nil {
31 t.Fatal(err)
32 }
33 handle, err := windows.CreateFile(name, windows.FILE_READ_ATTRIBUTES|windows.FILE_WRITE_ATTRIBUTES,
34 windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE,
35 nil, windows.OPEN_EXISTING, 0, 0)
36 if err != nil {
37 t.Fatal(err)
38 }
39 defer windows.CloseHandle(handle)
40 var basic windowsFileBasicInfo
41 if err := windows.GetFileInformationByHandleEx(handle, windows.FileBasicInfo, (*byte)(unsafe.Pointer(&basic)), uint32(unsafe.Sizeof(basic))); err != nil {
42 t.Fatal(err)
43 }
44 // Two writes can share a filesystem timestamp tick. Set a distinct native
45 // ChangeTime explicitly so this checks the version inputs, not clock speed.
46 change := windowsFileBasicInfo{ChangeTime: basic.ChangeTime + 20_000_000}
47 if err := windows.SetFileInformationByHandle(handle, windows.FileBasicInfo, (*byte)(unsafe.Pointer(&change)), uint32(unsafe.Sizeof(change))); err != nil {
48 t.Fatal(err)
49 }
50 afterInfo, err := os.Stat(path)
51 if err != nil {
52 t.Fatal(err)
53 }
54 afterTarget, after := DiskSnapshot(path, afterInfo)
55 if target != afterTarget || info.Size() != afterInfo.Size() || !info.ModTime().Equal(afterInfo.ModTime()) {
56 t.Fatal("ChangeTime fixture unexpectedly changed identity, size or mtime")
57 }
58 if before == after {
59 t.Fatal("native ChangeTime was omitted from the version")
60 }
61 }
62
63 func TestWindowsSnapshotDetectsSameSizeReplacementWithRestoredMtime(t *testing.T) {
64 path := filepath.Join(t.TempDir(), "replacement.txt")
65 if err := os.WriteFile(path, []byte("one"), 0o600); err != nil {
66 t.Fatal(err)
67 }
68 info, err := os.Stat(path)
69 if err != nil {
70 t.Fatal(err)
71 }
72 target, before := DiskSnapshot(path, info)
73 // Retain the old file so its native identity cannot be recycled for the new one.
74 if err := os.Rename(path, path+".old"); err != nil {
75 t.Fatal(err)
76 }
77 if err := os.WriteFile(path, []byte("two"), 0o600); err != nil {
78 t.Fatal(err)
79 }
80 if err := os.Chtimes(path, info.ModTime(), info.ModTime()); err != nil {
81 t.Fatal(err)
82 }
83 afterInfo, err := os.Stat(path)
84 if err != nil {
85 t.Fatal(err)
86 }
87 afterTarget, after := DiskSnapshot(path, afterInfo)
88 if info.Size() != afterInfo.Size() || !info.ModTime().Equal(afterInfo.ModTime()) {
89 t.Fatal("replacement fixture must preserve size and mtime")
90 }
91 if target.Key == afterTarget.Key || before == after {
92 t.Fatal("replacement did not change native identity and version")
93 }
94 }
95
96 func TestWindowsHandleSnapshotUsesTheSourceHandle(t *testing.T) {
97 path := filepath.Join(t.TempDir(), "handle-version.txt")
98 if err := os.WriteFile(path, []byte("source"), 0o600); err != nil {
99 t.Fatal(err)
100 }
101 file, err := os.Open(path)
102 if err != nil {
103 t.Fatal(err)
104 }
105 defer file.Close()
106 info, err := file.Stat()
107 if err != nil {
108 t.Fatal(err)
109 }
110 target, version := DiskHandleSnapshot(path, file, info)
111 if !strings.Contains(target.Key, "volume=") || !strings.Contains(target.Key, "fileindex=") {
112 t.Fatalf("handle target lacks native identity: %q", target.Key)
113 }
114 if version == "" {
115 t.Fatal("handle snapshot returned an empty version")
116 }
117 }
118
119 func TestWindowsSnapshotDetectsSecurityDescriptorChange(t *testing.T) {
120 path := filepath.Join(t.TempDir(), "acl-version.txt")
121 if err := os.WriteFile(path, []byte("unchanged"), 0o600); err != nil {
122 t.Fatal(err)
123 }
124 info, err := os.Stat(path)
125 if err != nil {
126 t.Fatal(err)
127 }
128 _, before := DiskSnapshot(path, info)
129
130 // os.Chmod does not represent NTFS ACL changes. Disabling inheritance
131 // changes the real security descriptor while preserving file contents.
132 cmd := exec.Command("icacls.exe", path, "/inheritance:d")
133 if output, err := cmd.CombinedOutput(); err != nil {
134 t.Fatalf("change ACL inheritance: %v: %s", err, output)
135 }
136 t.Cleanup(func() { _ = exec.Command("icacls.exe", path, "/inheritance:e").Run() })
137 afterInfo, err := os.Stat(path)
138 if err != nil {
139 t.Fatal(err)
140 }
141 _, after := DiskSnapshot(path, afterInfo)
142 if before == after {
143 t.Fatal("security descriptor change did not advance the native version")
144 }
145 }
146
146 lines GO