From 402f39da91748619e7746ce2b91a92375c342a11 Mon Sep 17 00:00:00 2001 From: Terence Boldt Date: Sun, 6 Mar 2022 05:29:33 -0500 Subject: [PATCH] Fix volume bitmap not writing full blocks (#8) --- main.go | 11 ++++- prodos/basic.go | 93 +++++++++++++++++++++++++++++++++++++++++++ prodos/bitmap.go | 35 ++++++++++++++-- prodos/bitmap_test.go | 29 ++++++++++++++ prodos/directory.go | 2 +- prodos/file.go | 82 ++++++++++++++++++++++++++++---------- prodos/file_test.go | 64 +++++++++++++++++++++++++++++ prodos/time.go | 4 +- 8 files changed, 292 insertions(+), 28 deletions(-) create mode 100644 prodos/file_test.go diff --git a/main.go b/main.go index dc9ca20..f68adfa 100644 --- a/main.go +++ b/main.go @@ -17,7 +17,7 @@ import ( "github.com/tjboldt/ProDOS-Utilities/prodos" ) -const version = "0.3.0" +const version = "0.3.1" func main() { var fileName string @@ -164,6 +164,15 @@ func main() { } defer file.Close() prodos.DeleteFile(file, pathName) + case "dumpfile": + file, err := os.OpenFile(fileName, os.O_RDWR, 0755) + if err != nil { + fmt.Printf("Failed to open drive image %s:\n %s", fileName, err) + os.Exit(1) + } + defer file.Close() + fileEntry, err := prodos.GetFileEntry(file, pathName) + prodos.DumpFileEntry(fileEntry) default: fmt.Printf("Invalid command: %s\n\n", command) flag.PrintDefaults() diff --git a/prodos/basic.go b/prodos/basic.go index 803256c..d5daf8f 100644 --- a/prodos/basic.go +++ b/prodos/basic.go @@ -160,3 +160,96 @@ func ConvertBasicToText(basic []byte) string { } } } + +// func ConvertTextToBasic(text string) ([]byte, error) { +// // convert line endings +// text = strings.Replace(text, "\r\n", "\n", -1) +// text = strings.Replace(text, "\r", "\n", -1) + +// const startState = 0 +// const lineNumberState = 1 +// const tokenCheckState = 2 +// const literalState = 3 +// const stringState = 4 +// const dataState = 5 +// const endOfLineState = 6 + +// state := startState + +// currentByte := 0x0801 +// var lineNumberString string +// var tokenString string + +// basicFile := new(bytes.Buffer) +// basicLine := new(bytes.Buffer) + +// // parse character by character +// for _, c := range text { + +// // skip initial whitespace and look for the start of a line number +// if state == startState { +// if c == ' ' { +// continue +// } +// if c >= '0' && c <= '9' { +// state = lineNumberState +// } else { +// return nil, errors.New("unexpected character before line number") +// } +// } + +// // parse line number +// if state == lineNumberState { +// if c >= '0' && c <= '9' { +// lineNumberString += string(c) +// } else { +// lineNumber, err := strconv.ParseUint(lineNumberString, 10, 16) +// if err != nil { +// return nil, err +// } +// basicLine.WriteByte(byte(lineNumber % 256)) // low byte +// basicLine.WriteByte(byte(lineNumber / 256)) // high byte +// tokenString = "" +// tokenByte = 0 +// state = tokenCheckState +// } +// } + +// if state == tokenCheckState { +// // skip initial whitespace +// if c == ' ' && len(tokenString) == 0 { +// continue +// } + +// // finish parsing token if +// if c == '\n' { +// state = endOfLineState +// } else if c == '"' { +// state = stringState +// } + +// } +// } + +// return basicFile.Bytes(), nil +// } + +// func writeTokenOrBytes(parseString string, basicBytes []byte) bool { +// if len(parseString) == 0 { +// return false +// } + +// upperToken := strings.ToUpper(parseString) + +// for tokenByte, token := range tokens { +// if upperToken == token { +// return tokenByte +// } +// } + +// if tokenByte > 0 { +// basicBytes.WriteByte(tokenByte) +// } + +// return 0 +// } diff --git a/prodos/bitmap.go b/prodos/bitmap.go index bf0fc61..ef2b298 100644 --- a/prodos/bitmap.go +++ b/prodos/bitmap.go @@ -8,6 +8,7 @@ package prodos import ( + "fmt" "io" ) @@ -64,11 +65,35 @@ func writeVolumeBitmap(readerWriter ReaderWriterAt, bitmap []byte) error { if err != nil { return err } - volumeHeader := parseVolumeHeader(headerBlock) - for i := 0; i < len(bitmap)/512; i++ { - WriteBlock(readerWriter, volumeHeader.BitmapStartBlock+i, bitmap[i*512:i*512+512]) + volumeHeader := parseVolumeHeader(headerBlock) + totalBitmapBytes := volumeHeader.TotalBlocks / 8 + if volumeHeader.TotalBlocks%8 > 0 { + totalBitmapBytes++ } + + totalBitmapBlocks := totalBitmapBytes / 512 + + if totalBitmapBytes%512 > 0 { + totalBitmapBlocks++ + } + + for i := 0; i < totalBitmapBlocks; i++ { + bitmapBlock, err := ReadBlock(readerWriter, i+volumeHeader.BitmapStartBlock) + if err != nil { + return err + } + + for j := 0; j < 512 && i*512+j < totalBitmapBytes; j++ { + bitmapBlock[j] = bitmap[i*512+j] + } + + err = WriteBlock(readerWriter, volumeHeader.BitmapStartBlock+i, bitmapBlock) + if err != nil { + return err + } + } + return nil } @@ -122,6 +147,10 @@ func findFreeBlocks(volumeBitmap []byte, numberOfBlocks int) []int { blocks[blocksFound] = i blocksFound++ if blocksFound == numberOfBlocks { + for i := 0; i < len(blocks); i++ { + fmt.Printf("%04X ", blocks[i]) + } + fmt.Printf("\n") return blocks } } diff --git a/prodos/bitmap_test.go b/prodos/bitmap_test.go index c73fad8..ac152b5 100644 --- a/prodos/bitmap_test.go +++ b/prodos/bitmap_test.go @@ -53,3 +53,32 @@ func TestCheckFreeBlockInVolumeBitmap(t *testing.T) { }) } } + +func TestMarkBlockInVolumeBitmap(t *testing.T) { + var tests = []struct { + blocks int + want bool + }{ + {0, false}, // boot block + {1, false}, // SOS boot block + {2, false}, // volume root + {21, false}, // end of volume bitmap + {22, true}, // beginning of free space + {999, false}, // end of volume bitmap + {8192, true}, // more free space + {65534, true}, // last free block + {65535, false}, // can't use last block because volume size is 0xFFFF, not 0x10000 + } + + for _, tt := range tests { + testname := fmt.Sprintf("%d", tt.blocks) + t.Run(testname, func(t *testing.T) { + volumeBitMap := createVolumeBitmap(65535) + markBlockInVolumeBitmap(volumeBitMap, 999) + ans := checkFreeBlockInVolumeBitmap(volumeBitMap, tt.blocks) + if ans != tt.want { + t.Errorf("got %t, want %t", ans, tt.want) + } + }) + } +} diff --git a/prodos/directory.go b/prodos/directory.go index 17ff3a5..5fae6e2 100644 --- a/prodos/directory.go +++ b/prodos/directory.go @@ -117,7 +117,7 @@ func getFreeFileEntryInDirectory(reader io.ReaderAt, directory string) (FileEntr // if we ran out of blocks in the directory, return empty // TODO: expand the directory to add more entries if blockNumber == 0 { - return FileEntry{}, errors.New("No free file entries found") + return FileEntry{}, errors.New("no free file entries found") } // else read the next block in the directory buffer, err = ReadBlock(reader, blockNumber) diff --git a/prodos/file.go b/prodos/file.go index c439cdb..752d8ad 100644 --- a/prodos/file.go +++ b/prodos/file.go @@ -9,6 +9,7 @@ package prodos import ( "errors" + "fmt" "io" "strings" "time" @@ -16,7 +17,7 @@ import ( // LoadFile loads in a file from a ProDOS volume into a byte array func LoadFile(reader io.ReaderAt, path string) ([]byte, error) { - fileEntry, err := getFileEntry(reader, path) + fileEntry, err := GetFileEntry(reader, path) if err != nil { return nil, err } @@ -45,7 +46,7 @@ func LoadFile(reader io.ReaderAt, path string) ([]byte, error) { func WriteFile(readerWriter ReaderWriterAt, path string, fileType int, auxType int, buffer []byte) error { directory, fileName := GetDirectoryAndFileNameFromPath(path) - existingFileEntry, _ := getFileEntry(readerWriter, path) + existingFileEntry, _ := GetFileEntry(readerWriter, path) if existingFileEntry.StorageType != StorageDeleted { DeleteFile(readerWriter, path) } @@ -56,6 +57,11 @@ func WriteFile(readerWriter ReaderWriterAt, path string, fileType int, auxType i return err } + for i := 0; i < len(blockList); i++ { + fmt.Printf("%04X ", blockList[i]) + } + fmt.Printf("\n") + // seedling file if len(buffer) <= 0x200 { WriteBlock(readerWriter, blockList[0], buffer) @@ -66,9 +72,13 @@ func WriteFile(readerWriter ReaderWriterAt, path string, fileType int, auxType i writeSaplingFile(readerWriter, buffer, blockList) } - // TODO: add tree file - if len(buffer) > 0x20000 { - return errors.New("files > 128KB not supported yet") + // tree file needs master index and index blocks + if len(buffer) > 0x20000 && len(buffer) <= 0x1000000 { + writeTreeFile(readerWriter, buffer, blockList) + } + + if len(buffer) > 0x1000000 { + return errors.New("files > 16MB not supported by ProDOS") } updateVolumeBitmap(readerWriter, blockList) @@ -111,15 +121,15 @@ func WriteFile(readerWriter ReaderWriterAt, path string, fileType int, auxType i // DeleteFile deletes a file from a ProDOS volume func DeleteFile(readerWriter ReaderWriterAt, path string) error { - fileEntry, err := getFileEntry(readerWriter, path) + fileEntry, err := GetFileEntry(readerWriter, path) if err != nil { - return errors.New("File not found") + return errors.New("file not found") } if fileEntry.StorageType == StorageDeleted { - return errors.New("File already deleted") + return errors.New("file already deleted") } if fileEntry.StorageType == StorageDirectory { - return errors.New("Directory deletion not supported") + return errors.New("directory deletion not supported") } // free the blocks @@ -173,15 +183,20 @@ func GetDirectoryAndFileNameFromPath(path string) (string, string) { } func updateVolumeBitmap(readerWriter ReaderWriterAt, blockList []int) error { + for i := 0; i < len(blockList); i++ { + fmt.Printf("%04X ", blockList[i]) + } + fmt.Printf("\n") + volumeBitmap, err := ReadVolumeBitmap(readerWriter) if err != nil { + fmt.Printf("%s", err) return err } for i := 0; i < len(blockList); i++ { markBlockInVolumeBitmap(volumeBitmap, blockList[i]) } - writeVolumeBitmap(readerWriter, volumeBitmap) - return nil + return writeVolumeBitmap(readerWriter, volumeBitmap) } func writeSaplingFile(writer io.WriterAt, buffer []byte, blockList []int) { @@ -216,6 +231,10 @@ func writeSaplingFile(writer io.WriterAt, buffer []byte, blockList []int) { } } +func writeTreeFile(writer io.WriterAt, buffer []byte, blockList []int) { + +} + // Returns all blocks, including index blocks func getBlocklist(reader io.ReaderAt, fileEntry FileEntry) ([]int, error) { blocks := make([]int, fileEntry.BlocksUsed) @@ -273,39 +292,60 @@ func getDataBlocklist(reader io.ReaderAt, fileEntry FileEntry) ([]int, error) { blocks[i] = int(index[i]) + int(index[i+256])*256 } return blocks, nil + // case StorageTree: + // blocks := make([]int, fileEntry.BlocksUsed-fileEntry.BlocksUsed/256-1) + // masterIndex := ReadBlock(reader, fileEntry.KeyPointer) + // for i := 0; i < 128; i++ { + // blockNumber := 0 + // blocks[j] = int(index[i]) + int(index[i+256])*256 + + // } } - return nil, errors.New("Unsupported file storage type") + return nil, errors.New("unsupported file storage type") } func createBlockList(reader io.ReaderAt, fileSize int) ([]int, error) { numberOfBlocks := fileSize / 512 + //fmt.Printf("Number of blocks %d\n", numberOfBlocks) + if fileSize%512 > 0 { + //fmt.Printf("Adding block for partial usage\n") numberOfBlocks++ } + if fileSize > 0x200 && fileSize <= 0x20000 { + //fmt.Printf("Adding index block for sapling file\n") numberOfBlocks++ // add index block } - if fileSize > 0x20000 { - // add master index block - numberOfBlocks++ - // add index blocks for each 128 blocks - numberOfBlocks += numberOfBlocks / 128 + + if fileSize > 0x20000 && fileSize < 0x1000000 { + //fmt.Printf("Tree file\n") + // add index blocks for each 256 blocks + numberOfBlocks += numberOfBlocks / 256 // add index block for any remaining blocks - if numberOfBlocks%128 > 0 { + if numberOfBlocks%256 > 0 { numberOfBlocks++ } + // add master index block + numberOfBlocks++ } + if fileSize > 0x1000000 { + return nil, errors.New("file size too large") + } + volumeBitmap, err := ReadVolumeBitmap(reader) if err != nil { return nil, err } + + //fmt.Printf("findFreeBlocks %d\n", numberOfBlocks) blockList := findFreeBlocks(volumeBitmap, numberOfBlocks) return blockList, nil } -func getFileEntry(reader io.ReaderAt, path string) (FileEntry, error) { +func GetFileEntry(reader io.ReaderAt, path string) (FileEntry, error) { directory, fileName := GetDirectoryAndFileNameFromPath(path) _, _, fileEntries, err := ReadDirectory(reader, directory) if err != nil { @@ -313,7 +353,7 @@ func getFileEntry(reader io.ReaderAt, path string) (FileEntry, error) { } if fileEntries == nil || len(fileEntries) == 0 { - return FileEntry{}, errors.New("File entry not found") + return FileEntry{}, errors.New("file entry not found") } var fileEntry FileEntry @@ -325,7 +365,7 @@ func getFileEntry(reader io.ReaderAt, path string) (FileEntry, error) { } if fileEntry.StorageType == StorageDeleted { - return FileEntry{}, errors.New("File not found") + return FileEntry{}, errors.New("file not found") } return fileEntry, nil diff --git a/prodos/file_test.go b/prodos/file_test.go new file mode 100644 index 0000000..3736093 --- /dev/null +++ b/prodos/file_test.go @@ -0,0 +1,64 @@ +// Copyright Terence J. Boldt (c)2021-2022 +// Use of this source code is governed by an MIT +// license that can be found in the LICENSE file. + +package prodos + +import ( + "fmt" + "testing" +) + +func TestCreatBlocklist(t *testing.T) { + var tests = []struct { + fileSize int + wantBlocks int + }{ + {1, 1}, + {512, 1}, + {513, 3}, + {2048, 5}, + {2049, 6}, + {17128, 35}, + } + + virtualDisk := NewMemoryFile(0x2000000) + CreateVolume(virtualDisk, "VIRTUAL.DISK", 0xFFFE) + + for _, tt := range tests { + testname := fmt.Sprintf("%d", tt.fileSize) + t.Run(testname, func(t *testing.T) { + blockList, err := createBlockList(virtualDisk, tt.fileSize) + + if err != nil { + t.Error("got error, want nil") + } + if len(blockList) != tt.wantBlocks { + t.Errorf("got %d blocks, want %d", len(blockList), tt.wantBlocks) + } + }) + } +} + +func TestUpdateVolumeBitmap(t *testing.T) { + blockList := []int{10, 11, 12, 100, 120} + + virtualDisk := NewMemoryFile(0x2000000) + CreateVolume(virtualDisk, "VIRTUAL.DISK", 0xFFFE) + updateVolumeBitmap(virtualDisk, blockList) + + for _, tt := range blockList { + testname := fmt.Sprintf("%d", tt) + t.Run(testname, func(t *testing.T) { + + volumeBitmap, err := ReadVolumeBitmap(virtualDisk) + if err != nil { + t.Error("got error, want nil") + } + free := checkFreeBlockInVolumeBitmap(volumeBitmap, tt) + if free { + t.Errorf("got true, want false") + } + }) + } +} diff --git a/prodos/time.go b/prodos/time.go index 8912840..ff4fb0d 100644 --- a/prodos/time.go +++ b/prodos/time.go @@ -11,14 +11,14 @@ import ( ) // DateTimeToProDOS converts Time to ProDOS date time -// 49041 ($BF91) 49040 ($BF90) +// 49041 ($BF91) 49040 ($BF90) // // 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 // +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ // DATE: | year | month | day | // +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ // -// 49043 ($BF93) 49042 ($BF92) +// 49043 ($BF93) 49042 ($BF92) // // 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 // +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+