package usn import ( "encoding/binary" "errors" "os" "path/filepath" "strings" "syscall" "testing" "unicode/utf16" "b612.me/win32api" ) func TestGetPointerUsesSliceLength(t *testing.T) { buf := make([]uint16, 3, 16) _, size, err := getPointer(buf) if err != nil { t.Fatalf("getPointer failed: %v", err) } if want := uintptr(len(buf)) * uintptr(2); size != want { t.Fatalf("slice size = %d, want %d", size, want) } } func TestParseUSNOutput(t *testing.T) { buf := buildTestUSNBuffer(1234, "hello.txt", false, 0x20) var got usnRecordData next, err := parseUSNOutput(buf, uint32(len(buf)), func(record usnRecordData) error { got = record return nil }) if err != nil { t.Fatalf("parseUSNOutput failed: %v", err) } if next != 1234 { t.Fatalf("next = %d, want 1234", next) } if got.FileName != "hello.txt" { t.Fatalf("FileName = %q, want %q", got.FileName, "hello.txt") } if got.FileReferenceNumber != 100 { t.Fatalf("FileReferenceNumber = %d, want 100", got.FileReferenceNumber) } if got.ParentFileReferenceNumber != 55 { t.Fatalf("ParentFileReferenceNumber = %d, want 55", got.ParentFileReferenceNumber) } if got.Reason != 0x20 { t.Fatalf("Reason = %#x, want %#x", got.Reason, 0x20) } } func TestParseUSNOutputRejectsShortRecord(t *testing.T) { buf := buildTestUSNBuffer(1, "bad", false, 0) binary.LittleEndian.PutUint32(buf[usnBufferHeaderSize:], uint32(usnRecordMinSize-2)) if _, err := parseUSNOutput(buf, uint32(len(buf)), func(usnRecordData) error { return nil }); err == nil { t.Fatal("expected parseUSNOutput to reject short record") } } func TestShouldPreferUSNFileName(t *testing.T) { tests := []struct { current string candidate string want bool }{ {current: "", candidate: "Program Files", want: true}, {current: "PROGRA~1", candidate: "Program Files", want: true}, {current: "Program Files", candidate: "PROGRA~1", want: false}, {current: "abc", candidate: "abcdef", want: true}, {current: "abcdef", candidate: "abc", want: false}, {current: "Program Files", candidate: "program files", want: false}, } for _, tt := range tests { if got := shouldPreferUSNFileName(tt.current, tt.candidate); got != tt.want { t.Fatalf("shouldPreferUSNFileName(%q, %q) = %v, want %v", tt.current, tt.candidate, got, tt.want) } } } func TestMergeUSNFileEntryPrefersLongName(t *testing.T) { current := FileEntry{Name: "PROGRA~1", Parent: 7} candidate := FileEntry{Name: "Program Files", Parent: 9} merged := mergeUSNFileEntry(current, candidate) if merged.Name != "Program Files" { t.Fatalf("Name = %q, want %q", merged.Name, "Program Files") } if merged.Parent != 9 { t.Fatalf("Parent = %d, want 9", merged.Parent) } } func TestMergeUSNFileEntryTracksRename(t *testing.T) { current := FileEntry{Name: "alpha.txt", Parent: 7} candidate := FileEntry{Name: "omega.txt", Parent: 7} merged := mergeUSNFileEntry(current, candidate) if merged.Name != "omega.txt" { t.Fatalf("Name = %q, want %q", merged.Name, "omega.txt") } } func TestFilterUSNFileMapUsesFinalName(t *testing.T) { fileMap := map[win32api.DWORDLONG]FileEntry{ 1: {Name: "Windows", Parent: 1, Type: 1}, 2: {Name: "Program Files", Parent: 1, Type: 0}, 3: {Name: "Temp", Parent: 1, Type: 0}, } filtered := filterUSNFileMap(fileMap, func(name string, _ bool) bool { return strings.Contains(name, "Program") }) if _, ok := filtered[1]; !ok { t.Fatal("expected directory entry to be retained") } if _, ok := filtered[2]; !ok { t.Fatal("expected matching file entry to be retained") } if _, ok := filtered[3]; ok { t.Fatal("did not expect non-matching file entry to be retained") } } func TestNeedPathCanonicalNameOverlay(t *testing.T) { if needPathCanonicalNameOverlay(map[win32api.DWORDLONG]FileEntry{ 1: {Name: "Program Files", Parent: 1}, }) { t.Fatal("did not expect overlay for long names only") } if !needPathCanonicalNameOverlay(map[win32api.DWORDLONG]FileEntry{ 1: {Name: "PROGRA~1", Parent: 1}, }) { t.Fatal("expected overlay when short name exists") } } func TestWindowsBaseName(t *testing.T) { if got := windowsBaseName(`C:\Program Files\`); got != "Program Files" { t.Fatalf("windowsBaseName returned %q", got) } if got := windowsBaseName(`C:\Windows\System32`); got != "System32" { t.Fatalf("windowsBaseName returned %q", got) } if got := windowsBaseName(`single`); got != "single" { t.Fatalf("windowsBaseName returned %q", got) } } func TestApplyPathCanonicalNamesUsesNormalizedPath(t *testing.T) { origNormalize := normalizePathForUSN defer func() { normalizePathForUSN = origNormalize }() normalizePathForUSN = func(path string) string { if strings.Contains(path, "PROGRA~1") { return strings.Replace(path, "PROGRA~1", "Program Files", 1) } return path } fileMap := map[win32api.DWORDLONG]FileEntry{ 1: {Name: "", Parent: 1, Type: 1}, 2: {Name: "PROGRA~1", Parent: 1, Type: 0}, } applyPathCanonicalNames("C:\\", fileMap) entry := fileMap[2] if entry.Name != "Program Files" { t.Fatalf("Name = %q, want %q", entry.Name, "Program Files") } if entry.Parent != 1 { t.Fatalf("Parent = %d, want 1", entry.Parent) } } func TestApplyPathCanonicalNamesSkipsWhenNotNeeded(t *testing.T) { origNormalize := normalizePathForUSN defer func() { normalizePathForUSN = origNormalize }() called := false normalizePathForUSN = func(path string) string { called = true return path } fileMap := map[win32api.DWORDLONG]FileEntry{ 2: {Name: "Program Files", Parent: 1, Type: 0}, } applyPathCanonicalNames("C:\\", fileMap) if called { t.Fatal("did not expect normalization when no short names exist") } } func TestFileStatFromIDWithfd(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "usn-by-id.txt") content := []byte("usn by id test") if err := os.WriteFile(path, content, 0600); err != nil { t.Fatalf("WriteFile failed: %v", err) } volume := filepath.VolumeName(path) + `\` info, err := GetDiskInfo(volume) if err != nil { t.Fatalf("GetDiskInfo failed: %v", err) } if !strings.EqualFold(info.Format, "NTFS") { t.Skipf("volume %s is %s, not NTFS", volume, info.Format) } file, err := os.Open(path) if err != nil { t.Fatalf("Open failed: %v", err) } defer file.Close() var handleInfo syscall.ByHandleFileInformation if err := syscall.GetFileInformationByHandle(syscall.Handle(file.Fd()), &handleInfo); err != nil { t.Fatalf("GetFileInformationByHandle failed: %v", err) } volumeHandle, err := CreateFile(`\\.\`+volume[:len(volume)-1], syscall.O_RDONLY, win32api.FILE_ATTRIBUTE_NORMAL) if err != nil { if errors.Is(err, syscall.ERROR_ACCESS_DENIED) { t.Skipf("opening volume handle requires extra privilege: %v", err) } t.Fatalf("CreateFile(volume) failed: %v", err) } defer syscall.Close(volumeHandle) fileID := win32api.DWORDLONG(uint64(handleInfo.FileIndexHigh)<<32 | uint64(handleInfo.FileIndexLow)) stat, err := fileStatFromIDWithfd(volumeHandle, fileID, filepath.Base(path), path, 0) if err != nil { t.Fatalf("fileStatFromIDWithfd failed: %v", err) } if stat.Name() != filepath.Base(path) { t.Fatalf("Name = %q, want %q", stat.Name(), filepath.Base(path)) } if stat.Size() != int64(len(content)) { t.Fatalf("Size = %d, want %d", stat.Size(), len(content)) } if stat.vol != handleInfo.VolumeSerialNumber || stat.idxhi != handleInfo.FileIndexHigh || stat.idxlo != handleInfo.FileIndexLow { t.Fatal("file identifiers do not match source handle info") } } func TestCollectUSNFileStatsSkipsFailedFetch(t *testing.T) { data := map[win32api.DWORDLONG]FileEntry{ 1: {Name: "keep-a.txt", Parent: 1, Type: 0}, 2: {Name: "drop-b.txt", Parent: 1, Type: 0}, 3: {Name: "keep-c", Parent: 1, Type: 1}, } got := collectUSNFileStats(data, nil, func(id win32api.DWORDLONG, entry FileEntry) (FileStat, error) { if id == 2 { return FileStat{}, errors.New("fetch failed") } stat := FileStat{name: entry.Name} if entry.Type == 1 { stat.FileAttributes = win32api.FILE_ATTRIBUTE_DIRECTORY } return stat, nil }) if len(got) != 2 { t.Fatalf("len(got) = %d, want 2", len(got)) } names := map[string]bool{} for _, stat := range got { names[stat.Name()] = true if stat.Name() == "" { t.Fatal("expected failed fetch entries to be skipped instead of zero-value placeholders") } } if !names["keep-a.txt"] || !names["keep-c"] { t.Fatalf("unexpected names: %+v", names) } if names["drop-b.txt"] { t.Fatal("did not expect failed fetch entry in results") } } func TestCollectUSNFileStatsAppliesFilter(t *testing.T) { data := map[win32api.DWORDLONG]FileEntry{ 1: {Name: "keep-file.txt", Parent: 1, Type: 0}, 2: {Name: "skip-file.txt", Parent: 1, Type: 0}, 3: {Name: "keep-dir", Parent: 1, Type: 1}, } got := collectUSNFileStats(data, func(name string, _ bool) bool { return strings.HasPrefix(name, "keep-") }, func(_ win32api.DWORDLONG, entry FileEntry) (FileStat, error) { stat := FileStat{name: entry.Name} if entry.Type == 1 { stat.FileAttributes = win32api.FILE_ATTRIBUTE_DIRECTORY } return stat, nil }) if len(got) != 2 { t.Fatalf("len(got) = %d, want 2", len(got)) } for _, stat := range got { if !strings.HasPrefix(stat.Name(), "keep-") { t.Fatalf("unexpected stat name %q", stat.Name()) } } } func TestCollectUSNFileStatsNilFilterIncludesAll(t *testing.T) { data := map[win32api.DWORDLONG]FileEntry{ 1: {Name: "a.txt", Parent: 1, Type: 0}, 2: {Name: "b.txt", Parent: 1, Type: 0}, 3: {Name: "c", Parent: 1, Type: 1}, } got := collectUSNFileStats(data, nil, func(_ win32api.DWORDLONG, entry FileEntry) (FileStat, error) { return FileStat{name: entry.Name}, nil }) if len(got) != len(data) { t.Fatalf("len(got) = %d, want %d", len(got), len(data)) } } func TestCollectUSNFileStatsNilFetchReturnsEmpty(t *testing.T) { data := map[win32api.DWORDLONG]FileEntry{ 1: {Name: "a.txt", Parent: 1, Type: 0}, 2: {Name: "b.txt", Parent: 1, Type: 0}, } got := collectUSNFileStats(data, nil, nil) if len(got) != 0 { t.Fatalf("len(got) = %d, want 0", len(got)) } } func buildTestUSNBuffer(next uint64, name string, isDir bool, reason uint32) []byte { encoded := utf16.Encode([]rune(name)) nameBytes := make([]byte, len(encoded)*2) for i, v := range encoded { binary.LittleEndian.PutUint16(nameBytes[i*2:], v) } recordLength := usnRecordMinSize + len(nameBytes) buf := make([]byte, usnBufferHeaderSize+recordLength) binary.LittleEndian.PutUint64(buf[:usnBufferHeaderSize], next) record := buf[usnBufferHeaderSize:] binary.LittleEndian.PutUint32(record, uint32(recordLength)) binary.LittleEndian.PutUint16(record[4:], 2) binary.LittleEndian.PutUint16(record[6:], 0) binary.LittleEndian.PutUint64(record[usnRecordOffsetFileReference:], 100) binary.LittleEndian.PutUint64(record[usnRecordOffsetParentReference:], 55) binary.LittleEndian.PutUint32(record[usnRecordOffsetReason:], reason) attrs := uint32(0) if isDir { attrs = win32api.FILE_ATTRIBUTE_DIRECTORY } binary.LittleEndian.PutUint32(record[usnRecordOffsetFileAttributes:], attrs) binary.LittleEndian.PutUint16(record[usnRecordOffsetFileNameLength:], uint16(len(nameBytes))) binary.LittleEndian.PutUint16(record[usnRecordOffsetFileNameOffset:], uint16(usnRecordMinSize)) copy(record[usnRecordMinSize:], nameBytes) return buf } func TestNormalizeDiskName(t *testing.T) { tests := map[string]string{ "c:": "C:\\", "c:\\temp": "C:\\", "D:/data": "D:\\", } for input, want := range tests { got, err := normalizeDiskName(input) if err != nil { t.Fatalf("normalizeDiskName(%q) returned error: %v", input, err) } if got != want { t.Fatalf("normalizeDiskName(%q) = %q, want %q", input, got, want) } } if _, err := normalizeDiskName(""); err == nil { t.Fatal("expected empty disk name error") } if _, err := normalizeDiskName("not-a-drive"); err == nil { t.Fatal("expected invalid disk name error") } } func TestUSNReasonStringUnknownHighBitDoesNotPanic(t *testing.T) { got := USNReasonString(0x80000000) if got == "" { t.Fatal("expected non-empty reason string") } }