diff --git a/filesystem/ext4/ext4.go b/filesystem/ext4/ext4.go index 660c3c1d..00538951 100644 --- a/filesystem/ext4/ext4.go +++ b/filesystem/ext4/ext4.go @@ -804,6 +804,14 @@ func (fs *FileSystem) OpenFile(p string, flag int) (filesystem.File, error) { }, nil } +func (fs *FileSystem) RemoveFile(p string) error { + return fmt.Errorf("not implemented") +} + +func (fs *FileSystem) RenameFile(p, newFileName string) error { + return fmt.Errorf("not implemented") +} + // Label read the volume label func (fs *FileSystem) Label() string { if fs.superblock == nil { diff --git a/filesystem/fat32/directory.go b/filesystem/fat32/directory.go index bd917b75..5c4652df 100644 --- a/filesystem/fat32/directory.go +++ b/filesystem/fat32/directory.go @@ -1,6 +1,7 @@ package fat32 import ( + "fmt" "time" ) @@ -41,7 +42,7 @@ func (d *Directory) entriesToBytes(bytesPerCluster int) ([]byte, error) { func (d *Directory) createEntry(name string, cluster uint32, dir bool) (*directoryEntry, error) { // is it a long filename or a short filename? var isLFN bool - // TODO: convertLfnSfn does not calculate if the short name conflicts and thus shoukld increment the last character + // TODO: convertLfnSfn does not calculate if the short name conflicts and thus should increment the last character // that should happen here, once we can look in the directory entry shortName, extension, isLFN, _ := convertLfnSfn(name) lfn := "" @@ -70,6 +71,75 @@ func (d *Directory) createEntry(name string, cluster uint32, dir bool) (*directo return &entry, nil } +// removeEntry removes an entry in the given directory +func (d *Directory) removeEntry(name string) error { + // is it a long filename or a short filename? + var isLFN bool + // TODO: convertLfnSfn does not calculate if the short name conflicts and thus should increment the last character + // that should happen here, once we can look in the directory entry + _, _, isLFN, _ = convertLfnSfn(name) + lfn := "" + if isLFN { + lfn = name + } + + removeEntryIndex := -1 + for i, entry := range d.entries { + if entry.filenameLong == lfn { // || entry.filenameShort == shortName do not compare SFN, since it is not incremented correctly + removeEntryIndex = i + } + } + + if removeEntryIndex == -1 { + return fmt.Errorf("cannot find entry for name %s", name) + } + + // remove the entry from the list + d.entries = append(d.entries[:removeEntryIndex], d.entries[removeEntryIndex+1:]...) + + return nil +} + +// renameEntry renames an entry in the given directory, and returns the handle to it +func (d *Directory) renameEntry(oldFileName, newFileName string) error { + // is it a long filename or a short filename? + var isLFN bool + // TODO: convertLfnSfn does not calculate if the short name conflicts and thus should increment the last character + // that should happen here, once we can look in the directory entry + _, _, isLFN, _ = convertLfnSfn(oldFileName) + lfn := "" + if isLFN { + lfn = oldFileName + } + + var newEntries []*directoryEntry + var isReplaced = false + for _, entry := range d.entries { + if entry.filenameLong == newFileName { + return fmt.Errorf("file with name %s already exists", newFileName) + } + if entry.filenameLong == lfn { // || entry.filenameShort == shortName do not compare SFN, since it is not incremented correctly + shortName, extension, isLFN, _ := convertLfnSfn(newFileName) + if isLFN { + lfn = newFileName + } + entry.filenameLong = lfn + entry.filenameShort = shortName + entry.fileExtension = extension + entry.modifyTime = time.Now() + isReplaced = true + } + newEntries = append(newEntries, entry) + } + if !isReplaced { + return fmt.Errorf("cannot find file entry for %s", oldFileName) + } + + d.entries = newEntries + + return nil +} + // createVolumeLabel create a volume label entry in the given directory, and return the handle to it func (d *Directory) createVolumeLabel(name string) (*directoryEntry, error) { // allocate a slot for the new filename in the existing directory diff --git a/filesystem/fat32/fat32.go b/filesystem/fat32/fat32.go index acb68ed1..4601ae76 100644 --- a/filesystem/fat32/fat32.go +++ b/filesystem/fat32/fat32.go @@ -599,6 +599,124 @@ func (fs *FileSystem) OpenFile(p string, flag int) (filesystem.File, error) { }, nil } +// RemoveFile removes a file from the filesystem +// +// returns an error if the file does not exist or cannot be removed +func (fs *FileSystem) RemoveFile(p string) error { + // get the path + dir := path.Dir(p) + filename := path.Base(p) + // if the dir == filename, then it is just / + if dir == filename { + return fmt.Errorf("cannot remove directory %s as file", p) + } + // get the directory entries + parentDir, entries, err := fs.readDirWithMkdir(dir, false) + if err != nil { + return fmt.Errorf("could not read directory entries for %s", dir) + } + // we now know that the directory exists, see if the file exists + var targetEntry *directoryEntry + for _, e := range entries { + shortName := e.filenameShort + if e.fileExtension != "" { + shortName += "." + e.fileExtension + } + if e.filenameLong != filename && shortName != filename { + continue + } + // cannot do anything with directories + if e.isSubdirectory { + return fmt.Errorf("cannot open directory %s as file", p) + } + // if we got this far, we have found the file + targetEntry = e + } + + // see if the file exists + // if the file does not exist, and is not opened for os.O_CREATE, return an error + if targetEntry == nil { + return fmt.Errorf("target file %s does not exist", p) + } + err = parentDir.removeEntry(filename) + if err != nil { + return fmt.Errorf("failed to remove file %s: %v", p, err) + } + + // we need to make sure that clusters are removed which may not be used anymore + _, err = fs.allocateSpace(uint64(parentDir.fileSize), parentDir.clusterLocation) + if err != nil { + return fmt.Errorf("failed to allocate clusters: %v", err) + } + + // write the directory entries to disk + err = fs.writeDirectoryEntries(parentDir) + if err != nil { + return fmt.Errorf("error writing directory file %s to disk: %v", p, err) + } + + return nil +} + +// RenameFile removes a file from the filesystem +// +// returns an error if the file does not exist or cannot be renamed +func (fs *FileSystem) RenameFile(p, newFileName string) error { + // get the path + dir := path.Dir(p) + filename := path.Base(p) + // if the dir == filename, then it is just / + if dir == filename { + return fmt.Errorf("cannot rename directory %s as file", p) + } + // get the directory entries + parentDir, entries, err := fs.readDirWithMkdir(dir, false) + if err != nil { + return fmt.Errorf("could not read directory entries for %s", dir) + } + // we now know that the directory exists, see if the file exists + var targetEntry *directoryEntry + for _, e := range entries { + shortName := e.filenameShort + if e.fileExtension != "" { + shortName += "." + e.fileExtension + } + if e.filenameLong != filename && shortName != filename { + continue + } + // cannot do anything with directories + if e.isSubdirectory { + return fmt.Errorf("cannot open directory %s as file", p) + } + // if we got this far, we have found the file + targetEntry = e + } + + // see if the file exists + // if the file does not exist, and is not opened for os.O_CREATE, return an error + if targetEntry == nil { + return fmt.Errorf("target file %s does not exist", p) + } + err = parentDir.renameEntry(filename, newFileName) + if err != nil { + return fmt.Errorf("failed to rename file %s: %v", p, err) + } + + // we need to make sure that clusters are removed which may not be used anymore + _, err = fs.allocateSpace(uint64(parentDir.fileSize), parentDir.clusterLocation) + if err != nil { + return fmt.Errorf("failed to allocate clusters: %v", err) + } + + // write the directory entries to disk + err = fs.writeDirectoryEntries(parentDir) + if err != nil { + return fmt.Errorf("error writing directory file %s to disk: %v", p, err) + } + + return nil +} + // Label get the label of the filesystem from the secial file in the root directory. // The label stored in the boot sector is ignored to mimic Windows behavior which // only stores and reads the label from the special file in the root directory. @@ -748,7 +866,7 @@ func (fs *FileSystem) mkSubdir(parent *Directory, name string) (*directoryEntry, } func (fs *FileSystem) writeDirectoryEntries(dir *Directory) error { - // we need to save the entries of theparent + // we need to save the entries of the parent b, err := dir.entriesToBytes(fs.bytesPerCluster) if err != nil { return fmt.Errorf("could not create a valid byte stream for a FAT32 Entries: %v", err) diff --git a/filesystem/fat32/fat32_test.go b/filesystem/fat32/fat32_test.go index 5b1bca19..674890a6 100644 --- a/filesystem/fat32/fat32_test.go +++ b/filesystem/fat32/fat32_test.go @@ -11,6 +11,7 @@ import ( "fmt" "io" "os" + "path/filepath" "strings" "testing" @@ -947,3 +948,285 @@ func TestFat32Label(t *testing.T) { } }) } + +func Test_RenameFile(t *testing.T) { + workingPath := "/" + oldFileName := "old.txt" + newFileName := "new.txt" + createFile := func(t *testing.T, fs *fat32.FileSystem, name, content string) { + origFile, err := fs.OpenFile(filepath.Join(workingPath, name), os.O_CREATE|os.O_RDWR) + if err != nil { + t.Fatalf("Could not create file %s: %+v", name, err) + } + defer origFile.Close() + // write test file + _, err = origFile.Write([]byte(content)) + if err != nil { + t.Fatalf("Could not Write file %s, %+v", name, err) + } + } + readFile := func(t *testing.T, fs *fat32.FileSystem, name string) string { + file, err := fs.OpenFile(filepath.Join(workingPath, name), os.O_RDONLY) + if err != nil { + t.Fatalf("file %s does not exist: %+v", name, err) + } + defer file.Close() + buf := &bytes.Buffer{} + _, err = io.Copy(buf, file) + if err != nil { + t.Fatalf("Could not read file %s: %+v", name, err) + } + return buf.String() + } + tests := []struct { + name string + hasError bool + pre func(t *testing.T, fs *fat32.FileSystem) + post func(t *testing.T, fs *fat32.FileSystem) + }{ + { + name: "simple renaming works without errors", + hasError: false, + pre: func(t *testing.T, fs *fat32.FileSystem) { + createFile(t, fs, oldFileName, "FooBar") + }, + post: func(t *testing.T, fs *fat32.FileSystem) { + //check if original file is there -> should not be the case + origFile, err := fs.OpenFile(oldFileName, os.O_RDONLY) + if err == nil { + defer origFile.Close() + t.Fatal("Original file is still there") + } + // check if new file is there -> should be the case + content := readFile(t, fs, newFileName) + if content != "FooBar" { + t.Fatalf("Content should be '%s', but is '%s'", "FooBar", content) + } + }, + }, + { + name: "destination file already exists", + hasError: true, + pre: func(t *testing.T, fs *fat32.FileSystem) { + createFile(t, fs, oldFileName, "FooBar") + //create destination file + createFile(t, fs, newFileName, "This should keep") + }, + post: func(t *testing.T, fs *fat32.FileSystem) { + // new file is not touched + content := readFile(t, fs, newFileName) + if content != "This should keep" { + t.Fatalf("Content should be '%s', but is '%s'", "This should keep", content) + } + + // old file is still there + content = readFile(t, fs, oldFileName) + if content != "FooBar" { + t.Fatalf("Content should be '%s', but is '%s'", "FooBar", content) + } + }, + }, + { + name: "source file does not exist", + hasError: true, + pre: func(t *testing.T, fs *fat32.FileSystem) { + // do not create orig file + }, + post: func(t *testing.T, fs *fat32.FileSystem) { + + }, + }, + { + name: "renaming long file to short file", + hasError: false, + pre: func(t *testing.T, fs *fat32.FileSystem) { + var s string + for i := 0; i < 255; i++ { + s += "a" + } + oldFileName = s + createFile(t, fs, s, "orig") + }, + post: func(t *testing.T, fs *fat32.FileSystem) { + oldFileName = "old.txt" + }, + }, + { + name: "renaming short file to long file", + hasError: false, + pre: func(t *testing.T, fs *fat32.FileSystem) { + var s string + for i := 0; i < 255; i++ { + s += "a" + } + newFileName = s + createFile(t, fs, oldFileName, "orig") + }, + post: func(t *testing.T, fs *fat32.FileSystem) { + newFileName = "new.txt" + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // get a mock filesystem image + f, err := tmpFat32(false, 0, 0) + if err != nil { + t.Fatal(err) + } + + if keepTmpFiles == "" { + defer os.Remove(f.Name()) + } else { + fmt.Println(f.Name()) + } + + fileInfo, err := f.Stat() + if err != nil { + t.Fatalf("error getting file info for tmpfile %s: %v", f.Name(), err) + } + + // create an empty filesystem + fs, err := fat32.Create(f, fileInfo.Size(), 0, 512, "go-diskfs") + if err != nil { + t.Fatalf("error creating fat32 filesystem: %v", err) + } + + test.pre(t, fs) + + err = fs.RenameFile(filepath.Join(workingPath, oldFileName), newFileName) + + if test.hasError { + if err == nil { + t.Fatal("No Error renaming file", err) + } + } else { + if err != nil { + t.Fatal("Error renaming file", err) + } + } + + test.post(t, fs) + }) + } +} + +func Test_RemoveFile(t *testing.T) { + workingPath := "/" + fileToRemove := "fileToRemove.txt" + createFile := func(t *testing.T, fs *fat32.FileSystem, name, content string) { + origFile, err := fs.OpenFile(filepath.Join(workingPath, name), os.O_CREATE|os.O_RDWR) + if err != nil { + t.Fatalf("Could not create file %s: %+v", name, err) + } + defer origFile.Close() + // write test file + _, err = origFile.Write([]byte(content)) + if err != nil { + t.Fatalf("Could not Write file %s, %+v", name, err) + } + } + tests := []struct { + name string + hasError bool + errorMsg string + pre func(t *testing.T, fs *fat32.FileSystem) + post func(t *testing.T, fs *fat32.FileSystem) + }{ + { + name: "simple remove works without", + hasError: false, + pre: func(t *testing.T, fs *fat32.FileSystem) { + createFile(t, fs, fileToRemove, "FooBar") + }, + post: func(t *testing.T, fs *fat32.FileSystem) { + //check if original file is there -> should not be the case + origFile, err := fs.OpenFile(fileToRemove, os.O_RDONLY) + if err == nil { + defer origFile.Close() + t.Fatal("Original file is still there") + } + }, + }, + { + name: "file to remove does not exist", + hasError: true, + errorMsg: "target file /fileToRemove.txt does not exist", + pre: func(t *testing.T, fs *fat32.FileSystem) { + // do not create any file + }, + post: func(t *testing.T, fs *fat32.FileSystem) { + + }, + }, + { + name: "removing multiple files", + hasError: false, + pre: func(t *testing.T, fs *fat32.FileSystem) { + var s string + for i := 0; i < 10240; i++ { + s += "this is a big file\n" + } + for i := 0; i < 50; i++ { + createFile(t, fs, fmt.Sprintf("file%d.txt", i), "small file") + } + createFile(t, fs, fileToRemove, s) + for i := 50; i < 100; i++ { + createFile(t, fs, fmt.Sprintf("file%d.txt", i), "small file") + } + }, + post: func(t *testing.T, fs *fat32.FileSystem) { + for i := 0; i < 100; i++ { + err := fs.RemoveFile(fmt.Sprintf("/file%d.txt", i)) + if err != nil { + t.Fatalf("expected no error, but got %v", err) + } + } + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // get a mock filesystem image + f, err := tmpFat32(false, 0, 0) + if err != nil { + t.Fatal(err) + } + + if keepTmpFiles == "" { + defer os.Remove(f.Name()) + } else { + fmt.Println(f.Name()) + } + + fileInfo, err := f.Stat() + if err != nil { + t.Fatalf("error getting file info for tmpfile %s: %v", f.Name(), err) + } + + // create an empty filesystem + fs, err := fat32.Create(f, fileInfo.Size(), 0, 512, "go-diskfs") + if err != nil { + t.Fatalf("error creating fat32 filesystem: %v", err) + } + + test.pre(t, fs) + + err = fs.RemoveFile(filepath.Join(workingPath, fileToRemove)) + + if test.hasError { + if err == nil { + t.Fatal("No Error renaming file", err) + } else if !strings.Contains(err.Error(), test.errorMsg) { + t.Fatalf("Error does not contain expected msg: %s. Original error: %v", test.errorMsg, err) + } + } else { + if err != nil { + t.Fatal("Error renaming file", err) + } + } + + test.post(t, fs) + }) + } +} diff --git a/filesystem/filesystem.go b/filesystem/filesystem.go index 2c1acfa6..cc1bfb45 100644 --- a/filesystem/filesystem.go +++ b/filesystem/filesystem.go @@ -16,6 +16,10 @@ type FileSystem interface { ReadDir(string) ([]os.FileInfo, error) // OpenFile open a handle to read or write to a file OpenFile(string, int) (File, error) + // RemoveFile removes a file from the filesystem + RemoveFile(p string) error + // RenameFile renames a file from the filesystem + RenameFile(p, newFileName string) error // Label get the label for the filesystem, or "" if none. Be careful to trim it, as it may contain // leading or following whitespace. The label is passed as-is and not cleaned up at all. Label() string diff --git a/filesystem/iso9660/iso9660.go b/filesystem/iso9660/iso9660.go index 64b28ed2..91367f01 100644 --- a/filesystem/iso9660/iso9660.go +++ b/filesystem/iso9660/iso9660.go @@ -414,6 +414,14 @@ func (fsm *FileSystem) OpenFile(p string, flag int) (filesystem.File, error) { return f, nil } +func (fs *FileSystem) RemoveFile(p string) error { + return fmt.Errorf("not implemented") +} + +func (fs *FileSystem) RenameFile(p, newFileName string) error { + return fmt.Errorf("not implemented") +} + // readDirectory - read directory entry on iso only (not workspace) func (fsm *FileSystem) readDirectory(p string) ([]*directoryEntry, error) { var ( diff --git a/filesystem/squashfs/squashfs.go b/filesystem/squashfs/squashfs.go index bdd8d5a7..55cf436d 100644 --- a/filesystem/squashfs/squashfs.go +++ b/filesystem/squashfs/squashfs.go @@ -379,6 +379,14 @@ func (fs *FileSystem) OpenFile(p string, flag int) (filesystem.File, error) { return f, nil } +func (fs *FileSystem) RemoveFile(p string) error { + return fmt.Errorf("not implemented") +} + +func (fs *FileSystem) RenameFile(p, newFileName string) error { + return fmt.Errorf("not implemented") +} + // readDirectory - read directory entry on squashfs only (not workspace) func (fs *FileSystem) readDirectory(p string) ([]*directoryEntry, error) { // use the root inode to find the location of the root direectory in the table