diff --git a/internal/fs/ea_windows.go b/internal/fs/ea_windows.go index e4b23d35a..08466c33f 100644 --- a/internal/fs/ea_windows.go +++ b/internal/fs/ea_windows.go @@ -211,8 +211,9 @@ var ( ) const ( - // noExtendedAttribsStatus is a constant value which indicates no extended attributes were found - noExtendedAttribsStatus = -1073741742 + // STATUS_NO_EAS_ON_FILE is a constant value which indicates EAs were requested for the file but it has no EAs. + // Windows NTSTATUS value: STATUS_NO_EAS_ON_FILE=0xC0000052 + STATUS_NO_EAS_ON_FILE = -1073741742 ) // GetFileEA retrieves the extended attributes for the file represented by `handle`. The @@ -228,7 +229,7 @@ func GetFileEA(handle windows.Handle) ([]ExtendedAttribute, error) { for { status := getFileEA(handle, &iosb, &buf[0], uint32(bufLen), false, 0, 0, nil, true) - if status == noExtendedAttribsStatus { + if status == STATUS_NO_EAS_ON_FILE { //If status is -1073741742, no extended attributes were found return nil, nil } diff --git a/internal/fs/ea_windows_test.go b/internal/fs/ea_windows_test.go index f9a4d9adf..14b7a2452 100644 --- a/internal/fs/ea_windows_test.go +++ b/internal/fs/ea_windows_test.go @@ -135,114 +135,103 @@ func TestSetFileEa(t *testing.T) { } } +// The code below was refactored from github.com/Microsoft/go-winio/blob/a7564fd482feb903f9562a135f1317fd3b480739/ea_test.go +// under MIT license. func TestSetGetFileEA(t *testing.T) { + testfilePath := setupTestFile(t) + defer cleanupTestFile(t, testfilePath) + + testEAs := generateTestEAs(t, 3, testfilePath) + fileHandle := openFile(t, testfilePath, windows.FILE_ATTRIBUTE_NORMAL) + defer closeFileHandle(t, fileHandle, testfilePath) + + testSetGetEA(t, fileHandle, testEAs, testfilePath) +} + +// The code is new code and reuses code refactored from github.com/Microsoft/go-winio/blob/a7564fd482feb903f9562a135f1317fd3b480739/ea_test.go +// under MIT license. +func TestSetGetFolderEA(t *testing.T) { + testfolderPath := setupTestFolder(t) + defer cleanupTestFolder(t, testfolderPath) + + testEAs := generateTestEAs(t, 3, testfolderPath) + fileHandle := openFile(t, testfolderPath, windows.FILE_ATTRIBUTE_NORMAL|windows.FILE_FLAG_BACKUP_SEMANTICS) + defer closeFileHandle(t, fileHandle, testfolderPath) + + testSetGetEA(t, fileHandle, testEAs, testfolderPath) +} + +func setupTestFile(t *testing.T) string { tempDir := t.TempDir() testfilePath := filepath.Join(tempDir, "testfile.txt") - // create temp file - testfile, err := os.Create(testfilePath) - if err != nil { + if _, err := os.Create(testfilePath); err != nil { t.Fatalf("failed to create temporary file: %s", err) } - defer func() { - err := testfile.Close() - if err != nil { - t.Logf("Error closing file %s: %v\n", testfile.Name(), err) - } - }() + return testfilePath +} - nAttrs := 3 - testEAs := make([]ExtendedAttribute, 3) - // generate random extended attributes for test +func setupTestFolder(t *testing.T) string { + tempDir := t.TempDir() + testfolderPath := filepath.Join(tempDir, "testfolder") + if err := os.Mkdir(testfolderPath, os.ModeDir); err != nil { + t.Fatalf("failed to create temporary folder: %s", err) + } + return testfolderPath +} + +func generateTestEAs(t *testing.T, nAttrs int, path string) []ExtendedAttribute { + testEAs := make([]ExtendedAttribute, nAttrs) for i := 0; i < nAttrs; i++ { - // EA name is automatically converted to upper case before storing, so - // when reading it back it returns the upper case name. To avoid test - // failures because of that keep the name upper cased. testEAs[i].Name = fmt.Sprintf("TESTEA%d", i+1) testEAs[i].Value = make([]byte, getRandomInt()) - _, err := rand.Read(testEAs[i].Value) - if err != nil { - t.Logf("Error reading rand for file %s: %v\n", testfilePath, err) + if _, err := rand.Read(testEAs[i].Value); err != nil { + t.Logf("Error reading rand for path %s: %v\n", path, err) } } + return testEAs +} - utf16Path := windows.StringToUTF16Ptr(testfilePath) - fileAccessRightReadWriteEA := (0x8 | 0x10) - fileHandle, err := windows.CreateFile(utf16Path, uint32(fileAccessRightReadWriteEA), 0, nil, windows.OPEN_EXISTING, windows.FILE_ATTRIBUTE_NORMAL, 0) +func openFile(t *testing.T, path string, attributes uint32) windows.Handle { + utf16Path := windows.StringToUTF16Ptr(path) + fileAccessRightReadWriteEA := uint32(0x8 | 0x10) + fileHandle, err := windows.CreateFile(utf16Path, fileAccessRightReadWriteEA, 0, nil, windows.OPEN_EXISTING, attributes, 0) if err != nil { t.Fatalf("open file failed with: %s", err) } - defer func() { - err := windows.Close(fileHandle) - if err != nil { - t.Logf("Error closing file handle %s: %v\n", testfilePath, err) - } - }() + return fileHandle +} - if err := SetFileEA(fileHandle, testEAs); err != nil { - t.Fatalf("set EA for file failed: %s", err) - } - - var readEAs []ExtendedAttribute - if readEAs, err = GetFileEA(fileHandle); err != nil { - t.Fatalf("get EA for file failed: %s", err) - } - - if !reflect.DeepEqual(readEAs, testEAs) { - t.Logf("expected: %+v, found: %+v\n", testEAs, readEAs) - t.Fatalf("EAs read from testfile don't match") +func closeFileHandle(t *testing.T, handle windows.Handle, path string) { + if err := windows.Close(handle); err != nil { + t.Logf("Error closing file handle %s: %v\n", path, err) } } -func TestSetGetFolderEA(t *testing.T) { - tempDir := t.TempDir() - testfolderPath := filepath.Join(tempDir, "testfolder") - // create temp folder - err := os.Mkdir(testfolderPath, os.ModeDir) +func testSetGetEA(t *testing.T, handle windows.Handle, testEAs []ExtendedAttribute, path string) { + if err := SetFileEA(handle, testEAs); err != nil { + t.Fatalf("set EA for path %s failed: %s", path, err) + } + + readEAs, err := GetFileEA(handle) if err != nil { - t.Fatalf("failed to create temporary file: %s", err) - } - - nAttrs := 3 - testEAs := make([]ExtendedAttribute, 3) - // generate random extended attributes for test - for i := 0; i < nAttrs; i++ { - // EA name is automatically converted to upper case before storing, so - // when reading it back it returns the upper case name. To avoid test - // failures because of that keep the name upper cased. - testEAs[i].Name = fmt.Sprintf("TESTEA%d", i+1) - testEAs[i].Value = make([]byte, getRandomInt()) - _, err := rand.Read(testEAs[i].Value) - if err != nil { - t.Logf("Error reading rand for file %s: %v\n", testfolderPath, err) - } - } - - utf16Path := windows.StringToUTF16Ptr(testfolderPath) - fileAccessRightReadWriteEA := (0x8 | 0x10) - fileHandle, err := windows.CreateFile(utf16Path, uint32(fileAccessRightReadWriteEA), 0, nil, windows.OPEN_EXISTING, windows.FILE_ATTRIBUTE_NORMAL|windows.FILE_FLAG_BACKUP_SEMANTICS, 0) - - if err != nil { - t.Fatalf("open folder failed with: %s", err) - } - defer func() { - err := windows.Close(fileHandle) - if err != nil { - t.Logf("Error closing file handle %s: %v\n", testfolderPath, err) - } - }() - - if err := SetFileEA(fileHandle, testEAs); err != nil { - t.Fatalf("set EA for folder failed: %s", err) - } - - var readEAs []ExtendedAttribute - if readEAs, err = GetFileEA(fileHandle); err != nil { - t.Fatalf("get EA for folder failed: %s", err) + t.Fatalf("get EA for path %s failed: %s", path, err) } if !reflect.DeepEqual(readEAs, testEAs) { t.Logf("expected: %+v, found: %+v\n", testEAs, readEAs) - t.Fatalf("EAs read from test folder don't match") + t.Fatalf("EAs read from path %s don't match", path) + } +} + +func cleanupTestFile(t *testing.T, path string) { + if err := os.Remove(path); err != nil { + t.Logf("Error removing file %s: %v\n", path, err) + } +} + +func cleanupTestFolder(t *testing.T, path string) { + if err := os.Remove(path); err != nil { + t.Logf("Error removing folder %s: %v\n", path, err) } } diff --git a/internal/restic/node_test.go b/internal/restic/node_test.go index 99ea48bbb..6e0f31e21 100644 --- a/internal/restic/node_test.go +++ b/internal/restic/node_test.go @@ -212,11 +212,7 @@ func TestNodeRestoreAt(t *testing.T) { extAttrArr := test.ExtendedAttributes // Iterate through the array using pointers for i := 0; i < len(extAttrArr); i++ { - // Get the pointer to the current element - namePtr := &extAttrArr[i].Name - - // Modify the value through the pointer - *namePtr = strings.ToUpper(*namePtr) + extAttrArr[i].Name = strings.ToUpper(extAttrArr[i].Name) } } diff --git a/internal/restic/node_windows.go b/internal/restic/node_windows.go index 881c394be..3e1dcae18 100644 --- a/internal/restic/node_windows.go +++ b/internal/restic/node_windows.go @@ -35,12 +35,12 @@ var ( ) // mknod is not supported on Windows. -func mknod(_ string, mode uint32, dev uint64) (err error) { +func mknod(_ string, _ uint32, _ uint64) (err error) { return errors.New("device nodes cannot be created on windows") } // Windows doesn't need lchown -func lchown(_ string, uid int, gid int) (err error) { +func lchown(_ string, _ int, _ int) (err error) { return nil } @@ -72,14 +72,12 @@ func (node Node) restoreSymlinkTimestamps(path string, utimes [2]syscall.Timespe // restore extended attributes for windows func (node Node) restoreExtendedAttributes(path string) (err error) { - eas := []fs.ExtendedAttribute{} - for _, attr := range node.ExtendedAttributes { - extr := new(fs.ExtendedAttribute) - extr.Name = attr.Name - extr.Value = attr.Value - eas = append(eas, *extr) - } - if len(eas) > 0 { + count := len(node.ExtendedAttributes) + if count > 0 { + eas := make([]fs.ExtendedAttribute, count) + for i, attr := range node.ExtendedAttributes { + eas[i] = fs.ExtendedAttribute{Name: attr.Name, Value: attr.Value} + } if errExt := restoreExtendedAttributes(node.Type, path, eas); errExt != nil { return errExt } @@ -90,25 +88,9 @@ func (node Node) restoreExtendedAttributes(path string) (err error) { // fill extended attributes in the node. This also includes the Generic attributes for windows. func (node *Node) fillExtendedAttributes(path string, _ bool) (err error) { var fileHandle windows.Handle - - //Get file handle for file or dir - if node.Type == "file" { - if strings.HasSuffix(filepath.Clean(path), `\`) { - return nil - } - utf16Path := windows.StringToUTF16Ptr(path) - fileAccessRightReadWriteEA := (0x8 | 0x10) - fileHandle, err = windows.CreateFile(utf16Path, uint32(fileAccessRightReadWriteEA), 0, nil, windows.OPEN_EXISTING, windows.FILE_ATTRIBUTE_NORMAL, 0) - } else if node.Type == "dir" { - utf16Path := windows.StringToUTF16Ptr(path) - fileAccessRightReadWriteEA := (0x8 | 0x10) - fileHandle, err = windows.CreateFile(utf16Path, uint32(fileAccessRightReadWriteEA), 0, nil, windows.OPEN_EXISTING, windows.FILE_ATTRIBUTE_NORMAL|windows.FILE_FLAG_BACKUP_SEMANTICS, 0) - } else { - return nil - } + fileHandle, err = getFileHandleForEA(node.Type, path) if err != nil { - err = errors.Errorf("open file failed for path: %s, with: %v", path, err) - return err + return errors.Errorf("get EA failed while opening file handle for path %v, with: %v", path, err) } defer func() { err := windows.CloseHandle(fileHandle) @@ -116,23 +98,19 @@ func (node *Node) fillExtendedAttributes(path string, _ bool) (err error) { debug.Log("Error closing file handle for %s: %v\n", path, err) } }() - //Get the windows Extended Attributes using the file handle - extAtts, err := fs.GetFileEA(fileHandle) + var extAtts []fs.ExtendedAttribute + extAtts, err = fs.GetFileEA(fileHandle) debug.Log("fillExtendedAttributes(%v) %v", path, extAtts) if err != nil { - debug.Log("open file failed for path: %s : %v", path, err) - return err - } else if len(extAtts) == 0 { + return errors.Errorf("get EA failed for path %v, with: %v", path, err) + } + if len(extAtts) == 0 { return nil } //Fill the ExtendedAttributes in the node using the name/value pairs in the windows EA for _, attr := range extAtts { - if err != nil { - err = errors.Errorf("can not obtain extended attribute for path %v, attr: %v, err: %v\n,", path, attr, err) - continue - } extendedAttr := ExtendedAttribute{ Name: attr.Name, Value: attr.Value, @@ -143,21 +121,30 @@ func (node *Node) fillExtendedAttributes(path string, _ bool) (err error) { return nil } -// restoreExtendedAttributes handles restore of the Windows Extended Attributes to the specified path. -// The Windows API requires setting of all the Extended Attributes in one call. -func restoreExtendedAttributes(nodeType, path string, eas []fs.ExtendedAttribute) (err error) { - var fileHandle windows.Handle +// Get file handle for file or dir for setting/getting EAs +func getFileHandleForEA(nodeType, path string) (handle windows.Handle, err error) { switch nodeType { case "file": utf16Path := windows.StringToUTF16Ptr(path) fileAccessRightReadWriteEA := (0x8 | 0x10) - fileHandle, err = windows.CreateFile(utf16Path, uint32(fileAccessRightReadWriteEA), 0, nil, windows.OPEN_EXISTING, windows.FILE_ATTRIBUTE_NORMAL, 0) + handle, err = windows.CreateFile(utf16Path, uint32(fileAccessRightReadWriteEA), 0, nil, windows.OPEN_EXISTING, windows.FILE_ATTRIBUTE_NORMAL, 0) case "dir": utf16Path := windows.StringToUTF16Ptr(path) fileAccessRightReadWriteEA := (0x8 | 0x10) - fileHandle, err = windows.CreateFile(utf16Path, uint32(fileAccessRightReadWriteEA), 0, nil, windows.OPEN_EXISTING, windows.FILE_ATTRIBUTE_NORMAL|windows.FILE_FLAG_BACKUP_SEMANTICS, 0) + handle, err = windows.CreateFile(utf16Path, uint32(fileAccessRightReadWriteEA), 0, nil, windows.OPEN_EXISTING, windows.FILE_ATTRIBUTE_NORMAL|windows.FILE_FLAG_BACKUP_SEMANTICS, 0) default: - return nil + return 0, nil + } + return handle, err +} + +// restoreExtendedAttributes handles restore of the Windows Extended Attributes to the specified path. +// The Windows API requires setting of all the Extended Attributes in one call. +func restoreExtendedAttributes(nodeType, path string, eas []fs.ExtendedAttribute) (err error) { + var fileHandle windows.Handle + fileHandle, err = getFileHandleForEA(nodeType, path) + if err != nil { + return errors.Errorf("set EA failed while opening file handle for path %v, with: %v", path, err) } defer func() { err := windows.CloseHandle(fileHandle) @@ -165,12 +152,10 @@ func restoreExtendedAttributes(nodeType, path string, eas []fs.ExtendedAttribute debug.Log("Error closing file handle for %s: %v\n", path, err) } }() - if err != nil { - err = errors.Errorf("open file failed for path %v, with: %v:\n", path, err) - } else if err = fs.SetFileEA(fileHandle, eas); err != nil { - err = errors.Errorf("set EA failed for path %v, with: %v:\n", path, err) + if err = fs.SetFileEA(fileHandle, eas); err != nil { + return errors.Errorf("set EA failed for path %v, with: %v", path, err) } - return err + return nil } type statT syscall.Win32FileAttributeData diff --git a/internal/restic/node_windows_test.go b/internal/restic/node_windows_test.go index f89e2aeee..29a42e9e2 100644 --- a/internal/restic/node_windows_test.go +++ b/internal/restic/node_windows_test.go @@ -311,23 +311,21 @@ func TestRestoreExtendedAttributes(t *testing.T) { test.OK(t, errors.Wrapf(err, "Error closing file for: %s", testPath)) }() - if len(node.ExtendedAttributes) > 0 { - extAttr, err := fs.GetFileEA(handle) - test.OK(t, errors.Wrapf(err, "Error getting extended attributes for: %s", testPath)) - test.Equals(t, len(node.ExtendedAttributes), len(extAttr)) + extAttr, err := fs.GetFileEA(handle) + test.OK(t, errors.Wrapf(err, "Error getting extended attributes for: %s", testPath)) + test.Equals(t, len(node.ExtendedAttributes), len(extAttr)) - for _, expectedExtAttr := range node.ExtendedAttributes { - var foundExtAttr *fs.ExtendedAttribute - for _, ea := range extAttr { - if strings.EqualFold(ea.Name, expectedExtAttr.Name) { - foundExtAttr = &ea - break + for _, expectedExtAttr := range node.ExtendedAttributes { + var foundExtAttr *fs.ExtendedAttribute + for _, ea := range extAttr { + if strings.EqualFold(ea.Name, expectedExtAttr.Name) { + foundExtAttr = &ea + break - } } - test.Assert(t, foundExtAttr != nil, "Expected extended attribute not found") - test.Equals(t, expectedExtAttr.Value, foundExtAttr.Value) } + test.Assert(t, foundExtAttr != nil, "Expected extended attribute not found") + test.Equals(t, expectedExtAttr.Value, foundExtAttr.Value) } } }