From 25d5b7e7b4611199da1e3b60f0ee5bfb83b199cf Mon Sep 17 00:00:00 2001 From: Avi Deitcher Date: Mon, 4 Dec 2023 01:33:17 -0800 Subject: [PATCH] iso rock ridge should handle creating with filenames longer than 30 chars (#190) Signed-off-by: Avi Deitcher --- filesystem/iso9660/directoryentry.go | 33 +++++- filesystem/iso9660/iso9660_test.go | 108 +++++++++++++++++- filesystem/iso9660/testdata/iso-in-folder/abc | 3 + .../iso-in-folder/subfolder1/hello.txt | 1 + .../iso9660/testdata/rock-ridge-in-folder/abc | 3 + .../rock-ridge-in-folder/subfolder1/hello.txt | 1 + ...-a-very-long-filename-longer-than-30-chars | 1 + 7 files changed, 143 insertions(+), 7 deletions(-) create mode 100644 filesystem/iso9660/testdata/iso-in-folder/abc create mode 100644 filesystem/iso9660/testdata/iso-in-folder/subfolder1/hello.txt create mode 100644 filesystem/iso9660/testdata/rock-ridge-in-folder/abc create mode 100644 filesystem/iso9660/testdata/rock-ridge-in-folder/subfolder1/hello.txt create mode 100644 filesystem/iso9660/testdata/rock-ridge-in-folder/this-is-a-file-with-a-very-long-filename-longer-than-30-chars diff --git a/filesystem/iso9660/directoryentry.go b/filesystem/iso9660/directoryentry.go index a6f538e0..43f03518 100644 --- a/filesystem/iso9660/directoryentry.go +++ b/filesystem/iso9660/directoryentry.go @@ -119,7 +119,7 @@ func (de *directoryEntry) toBytes(skipExt bool, ceBlocks []uint32) ([][]byte, er filenameBytes = []byte{0x01} default: // first validate the filename - err = validateFilename(de.filename, de.isSubdirectory) + err = validateFilename(de.filename, de.isSubdirectory, de.filesystem.suspEnabled) if err != nil { nametype := "filename" if de.isSubdirectory { @@ -549,16 +549,27 @@ func timeToBytes(t time.Time) []byte { } // convert a string to ascii bytes, but only accept valid d-characters -func validateFilename(s string, isDir bool) error { +func validateFilename(s string, isDir, suspExtension bool) error { var err error + if suspExtension { + err = validateSUSPFilename(s, isDir) + } else { + err = validateISOFilename(s, isDir) + } + return err +} + +// validateISOFilename validates a filename that is plain ISO9660-compliant (levels 2 & 3) +func validateISOFilename(s string, isDir bool) error { + var err error + // all allowed up to 30 characters, of A-Z,0-9,_ if isDir { - // directory only allowed up to 8 characters of A-Z,0-9,_ re := regexp.MustCompile("^[A-Z0-9_]{1,30}$") if !re.MatchString(s) { err = fmt.Errorf("directory name must be of up to 30 characters from A-Z0-9_") } } else { - // filename only allowed up to 8 characters of A-Z,0-9,_, plus an optional '.' plus up to 3 characters of A-Z,0-9,_, plus must have ";1" + // filename also allowed an optional '.' plus up to 3 characters of A-Z,0-9,_, plus must have ";1" re := regexp.MustCompile("^[A-Z0-9_]+(.[A-Z0-9_]*)?;1$") switch { case !re.MatchString(s): @@ -570,6 +581,20 @@ func validateFilename(s string, isDir bool) error { return err } +// validateSUSPFilename validates a filename that is Rock Ridge compliant +func validateSUSPFilename(s string, _ bool) error { + var err error + // all allowed up to 255 characters of any kind, except null (0x0) and '/' + re := regexp.MustCompile(`^[^\x00/]*$`) + switch { + case len(s) > 255: + err = fmt.Errorf("filename must be at most 255 characters") + case !re.MatchString(s): + err = fmt.Errorf("filename must not include / or null characters") + } + return err +} + // convert a string to a byte array, if all characters are valid ascii func stringToASCIIBytes(s string) ([]byte, error) { length := len(s) diff --git a/filesystem/iso9660/iso9660_test.go b/filesystem/iso9660/iso9660_test.go index bbda14f2..601dba86 100644 --- a/filesystem/iso9660/iso9660_test.go +++ b/filesystem/iso9660/iso9660_test.go @@ -10,9 +10,12 @@ import ( "io" "os" "path" + "path/filepath" "strings" "testing" + "github.com/diskfs/go-diskfs" + "github.com/diskfs/go-diskfs/disk" "github.com/diskfs/go-diskfs/filesystem" "github.com/diskfs/go-diskfs/filesystem/iso9660" ) @@ -479,12 +482,12 @@ func TestIso9660OpenFile(t *testing.T) { } for i, tt := range tests { for _, fs := range []*iso9660.FileSystem{fsTemp, fsUser} { - filepath := path.Join(fs.Workspace(), tt.path) + fullpath := path.Join(fs.Workspace(), tt.path) // remove any old file if it exists - ignore errors - _ = os.Remove(filepath) + _ = os.Remove(fullpath) // if the file is supposed to exist, create it and add its contents if tt.mode&os.O_CREATE != os.O_CREATE { - _ = os.WriteFile(filepath, []byte(baseContent), 0o600) + _ = os.WriteFile(fullpath, []byte(baseContent), 0o600) } header := fmt.Sprintf("%d: OpenFile(%s, %s, %t)", i, tt.path, getOpenMode(tt.mode), tt.beginning) readWriter, err := fs.OpenFile(tt.path, tt.mode) @@ -537,5 +540,104 @@ func TestIso9660OpenFile(t *testing.T) { } func TestIso9660Finalize(t *testing.T) { + var createISOFilesystem = func(inDir, outputFileName string, rockRidge bool) error { + var LogicalBlocksize diskfs.SectorSize = 2048 + // Create the disk image + // TODO: Explain why we need to use Raw here + mydisk, err := diskfs.Create(outputFileName, 100*1024, diskfs.Raw, LogicalBlocksize) + if err != nil { + return err + } + + // Create the ISO filesystem on the disk image + fspec := disk.FilesystemSpec{ + Partition: 0, + FSType: filesystem.TypeISO9660, + VolumeLabel: "label", + } + fs, err := mydisk.CreateFilesystem(fspec) + if err != nil { + return err + } + + // Walk the source folder to copy all files and folders to the ISO filesystem + err = filepath.Walk(inDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + relPath, err := filepath.Rel(inDir, path) + if err != nil { + return err + } + + // If the current path is a folder, create the folder in the ISO filesystem + if info.IsDir() { + // Create the directory in the ISO file + err = fs.Mkdir(relPath) + if err != nil { + return err + } + return nil + } + + // If the current path is a file, copy the file to the ISO filesystem + if !info.IsDir() { + // Open the file in the ISO file for writing + rw, err := fs.OpenFile(relPath, os.O_CREATE|os.O_RDWR) + if err != nil { + return err + } + + // Open the source file for reading + in, errorOpeningFile := os.Open(path) + if errorOpeningFile != nil { + return errorOpeningFile + } + defer in.Close() + + // Copy the contents of the source file to the ISO file + _, err = io.Copy(rw, in) + if err != nil { + return err + } + } + + return nil + }) + if err != nil { + return err + } + + iso, ok := fs.(*iso9660.FileSystem) + if !ok { + return fmt.Errorf("not an iso9660 filesystem") + } + opts := iso9660.FinalizeOptions{} + if rockRidge { + opts.RockRidge = true + } + return iso.Finalize(opts) + } + tests := []struct { + name string + inDir string + outputFileName string + rockRidge bool + }{ + {"normal", "testdata/iso-in-folder", "iso-image.iso", false}, + {"rock ridge", "testdata/rock-ridge-in-folder", "rock-ridge-image.iso", true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpDir := os.TempDir() + outfile := path.Join(tmpDir, tt.outputFileName) + defer os.RemoveAll(outfile) + err := createISOFilesystem(tt.inDir, outfile, tt.rockRidge) + if err != nil { + t.Errorf("Failed to create ISO filesystem: %v", err) + } + }) + } } diff --git a/filesystem/iso9660/testdata/iso-in-folder/abc b/filesystem/iso9660/testdata/iso-in-folder/abc new file mode 100644 index 00000000..8edb37e3 --- /dev/null +++ b/filesystem/iso9660/testdata/iso-in-folder/abc @@ -0,0 +1,3 @@ +abc +def +ghi diff --git a/filesystem/iso9660/testdata/iso-in-folder/subfolder1/hello.txt b/filesystem/iso9660/testdata/iso-in-folder/subfolder1/hello.txt new file mode 100644 index 00000000..c5042db3 --- /dev/null +++ b/filesystem/iso9660/testdata/iso-in-folder/subfolder1/hello.txt @@ -0,0 +1 @@ +Hello from subfolder! \ No newline at end of file diff --git a/filesystem/iso9660/testdata/rock-ridge-in-folder/abc b/filesystem/iso9660/testdata/rock-ridge-in-folder/abc new file mode 100644 index 00000000..8edb37e3 --- /dev/null +++ b/filesystem/iso9660/testdata/rock-ridge-in-folder/abc @@ -0,0 +1,3 @@ +abc +def +ghi diff --git a/filesystem/iso9660/testdata/rock-ridge-in-folder/subfolder1/hello.txt b/filesystem/iso9660/testdata/rock-ridge-in-folder/subfolder1/hello.txt new file mode 100644 index 00000000..c5042db3 --- /dev/null +++ b/filesystem/iso9660/testdata/rock-ridge-in-folder/subfolder1/hello.txt @@ -0,0 +1 @@ +Hello from subfolder! \ No newline at end of file diff --git a/filesystem/iso9660/testdata/rock-ridge-in-folder/this-is-a-file-with-a-very-long-filename-longer-than-30-chars b/filesystem/iso9660/testdata/rock-ridge-in-folder/this-is-a-file-with-a-very-long-filename-longer-than-30-chars new file mode 100644 index 00000000..4455ac66 --- /dev/null +++ b/filesystem/iso9660/testdata/rock-ridge-in-folder/this-is-a-file-with-a-very-long-filename-longer-than-30-chars @@ -0,0 +1 @@ +iso9660 only supports filenames up to 30 characters. Rock Ridge supports filenames up to 255 characters.