Add putallrecursive

This commit is contained in:
Terence Boldt 2023-01-22 10:01:57 -05:00
parent 1722e7b23a
commit 286964d3ee
7 changed files with 242 additions and 55 deletions

24
main.go
View File

@ -32,7 +32,7 @@ func main() {
var auxType int
flag.StringVar(&fileName, "d", "", "A ProDOS format drive image")
flag.StringVar(&pathName, "p", "", "Path name in ProDOS drive image (default is root of volume)")
flag.StringVar(&command, "c", "ls", "Command to execute: ls, get, put, rm, mkdir, readblock, writeblock, create, putall")
flag.StringVar(&command, "c", "ls", "Command to execute: ls, get, put, rm, mkdir, readblock, writeblock, create, putall, putallrecursive")
flag.StringVar(&outFileName, "o", "", "Name of file to write")
flag.StringVar(&inFileName, "i", "", "Name of file to read")
flag.IntVar(&volumeSize, "s", 65535, "Number of blocks to create the volume with (default 65535, 64 to 65535, 0x0040 to 0xFFFF hex input accepted)")
@ -62,13 +62,17 @@ func main() {
case "create":
create(fileName, volumeName, volumeSize)
case "putall":
putall(fileName, inFileName)
putall(fileName, inFileName, pathName, false)
case "putallrecursive":
putall(fileName, inFileName, pathName, true)
case "rm":
rm(fileName, pathName)
case "mkdir":
mkdir(fileName, pathName)
case "dumpfile":
dumpFile(fileName, pathName)
case "dumpdirectory":
dumpDirectory(fileName, pathName)
default:
fmt.Printf("Invalid command: %s\n\n", command)
flag.PrintDefaults()
@ -88,6 +92,18 @@ func dumpFile(fileName string, pathName string) {
prodos.DumpFileEntry(fileEntry)
}
func dumpDirectory(fileName string, pathName string) {
checkPathName(pathName)
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()
_, directoryheader, _, err := prodos.ReadDirectory(file, pathName)
prodos.DumpDirectoryHeader(directoryheader)
}
func mkdir(fileName string, pathName string) {
checkPathName(pathName)
file, err := os.OpenFile(fileName, os.O_RDWR, 0755)
@ -114,7 +130,7 @@ func rm(fileName string, pathName string) {
prodos.DeleteFile(file, pathName)
}
func putall(fileName string, inFileName string) {
func putall(fileName string, inFileName string, pathName string, recursive bool) {
if len(inFileName) == 0 {
inFileName = "."
}
@ -124,7 +140,7 @@ func putall(fileName string, inFileName string) {
os.Exit(1)
}
defer file.Close()
err = prodos.AddFilesFromHostDirectory(file, inFileName)
err = prodos.AddFilesFromHostDirectory(file, inFileName, pathName, recursive)
if err != nil {
fmt.Printf("failed to add host files: %s\n", err)
os.Exit(1)

View File

@ -131,7 +131,6 @@ func createVolumeBitmap(numberOfBlocks int) []byte {
markBlockInVolumeBitmap(volumeBitmap, i)
}
}
//DumpBlock(volumeBitmap)
return volumeBitmap
}

View File

@ -33,6 +33,7 @@ type VolumeHeader struct {
type DirectoryHeader struct {
PreviousBlock int
NextBlock int
IsSubDirectory bool
Name string
CreationTime time.Time
Version int
@ -77,6 +78,7 @@ type FileEntry struct {
AuxType int
ModifiedTime time.Time
HeaderPointer int
DirectoryBlock int
DirectoryOffset int
}
@ -95,6 +97,11 @@ func ReadDirectory(reader io.ReaderAt, path string) (VolumeHeader, DirectoryHead
path = fmt.Sprintf("/%s", volumeHeader.VolumeName)
}
// add volume name if not full path
if !strings.HasPrefix(path, "/") {
path = fmt.Sprintf("/%s/%s", volumeHeader.VolumeName, path)
}
path = strings.ToUpper(path)
paths := strings.Split(path, "/")
@ -106,12 +113,23 @@ func ReadDirectory(reader io.ReaderAt, path string) (VolumeHeader, DirectoryHead
return volumeHeader, directoryHeader, fileEntries, nil
}
// CreateDirectory creates a directory information of a specified path
// on a ProDOS image
func CreateDirectory(readerWriter ReaderWriterAt, path string) error {
if len(path) == 0 {
return errors.New("cannot create directory with path")
}
// add volume name if not full path
path, err := makeFullPath(path, readerWriter)
if err != nil {
return err
}
parentPath, newDirectory := GetDirectoryAndFileNameFromPath(path)
existingFileEntry, _ := GetFileEntry(readerWriter, path)
if existingFileEntry.StorageType != StorageDeleted {
//DeleteFile(readerWriter, path)
return errors.New("directory already exists")
}
@ -140,6 +158,8 @@ func CreateDirectory(readerWriter ReaderWriterAt, path string) error {
fileEntry.KeyPointer = blockList[0]
fileEntry.Access = 0b11100011
fileEntry.StorageType = StorageDirectory
fileEntry.Version = 0x24
fileEntry.MinVersion = 0x00
writeFileEntry(readerWriter, fileEntry)
@ -152,6 +172,7 @@ func CreateDirectory(readerWriter ReaderWriterAt, path string) error {
directoryEntry := DirectoryHeader{
PreviousBlock: 0,
NextBlock: 0,
IsSubDirectory: true,
Name: newDirectory,
CreationTime: time.Now(),
Version: 0x24,
@ -162,7 +183,7 @@ func CreateDirectory(readerWriter ReaderWriterAt, path string) error {
ActiveFileCount: 0,
StartingBlock: blockList[0],
ParentBlock: fileEntry.DirectoryBlock,
ParentEntry: fileEntry.DirectoryOffset,
ParentEntry: (fileEntry.DirectoryOffset - 0x04) / 0x27,
ParentEntryLength: 0x27,
}
@ -175,14 +196,26 @@ func CreateDirectory(readerWriter ReaderWriterAt, path string) error {
return nil
}
func getFreeFileEntryInDirectory(reader io.ReaderAt, directory string) (FileEntry, error) {
_, directoryHeader, _, err := ReadDirectory(reader, directory)
func makeFullPath(path string, reader io.ReaderAt) (string, error) {
if !strings.HasPrefix(path, "/") {
buffer, err := ReadBlock(reader, 0x0002)
if err != nil {
return "", err
}
volumeHeader := parseVolumeHeader(buffer)
path = fmt.Sprintf("/%s/%s", volumeHeader.VolumeName, path)
}
return path, nil
}
func getFreeFileEntryInDirectory(readerWriter ReaderWriterAt, directory string) (FileEntry, error) {
_, directoryHeader, _, err := ReadDirectory(readerWriter, directory)
if err != nil {
return FileEntry{}, err
}
//DumpDirectoryHeader(directoryHeader)
blockNumber := directoryHeader.StartingBlock
buffer, err := ReadBlock(reader, blockNumber)
buffer, err := ReadBlock(readerWriter, blockNumber)
if err != nil {
return FileEntry{}, err
}
@ -191,21 +224,28 @@ func getFreeFileEntryInDirectory(reader io.ReaderAt, directory string) (FileEntr
for {
if entryNumber > 13 {
blockNumber = int(buffer[2]) + int(buffer[3])*256
// if we ran out of blocks in the directory, return empty
// TODO: expand the directory to add more entries
if blockNumber == 0 {
nextBlockNumber := int(buffer[2]) + int(buffer[3])*256
// if we ran out of blocks in the directory, expand directory or fail
if nextBlockNumber == 0 {
if !directoryHeader.IsSubDirectory {
return FileEntry{}, errors.New("no free file entries found")
}
nextBlockNumber, err = expandDirectory(readerWriter, nextBlockNumber, buffer, blockNumber, directoryHeader)
if err != nil {
return FileEntry{}, err
}
}
blockNumber = nextBlockNumber
// else read the next block in the directory
buffer, err = ReadBlock(reader, blockNumber)
buffer, err = ReadBlock(readerWriter, blockNumber)
if err != nil {
return FileEntry{}, nil
}
entryOffset = 4
entryNumber = 1
}
fileEntry := parseFileEntry(buffer[entryOffset:entryOffset+40], blockNumber, entryOffset)
fileEntry := parseFileEntry(buffer[entryOffset:entryOffset+0x28], blockNumber, entryOffset)
if fileEntry.StorageType == StorageDeleted {
fileEntry.DirectoryBlock = blockNumber
@ -219,6 +259,51 @@ func getFreeFileEntryInDirectory(reader io.ReaderAt, directory string) (FileEntr
}
}
func expandDirectory(readerWriter ReaderWriterAt, nextBlockNumber int, buffer []byte, blockNumber int, directoryHeader DirectoryHeader) (int, error) {
volumeBitMap, err := ReadVolumeBitmap(readerWriter)
if err != nil {
errString := fmt.Sprintf("failed to get volume bitmap to expand directory: %s", err)
return 0, errors.New(errString)
}
blockList := findFreeBlocks(volumeBitMap, 1)
if len(blockList) != 1 {
return 0, errors.New("failed to get free block to expand directory")
}
nextBlockNumber = blockList[0]
buffer[0x02] = byte(nextBlockNumber & 0x00FF)
buffer[0x03] = byte(nextBlockNumber >> 8)
WriteBlock(readerWriter, blockNumber, buffer)
if err != nil {
errString := fmt.Sprintf("failed to write block to expand directory: %s", err)
return 0, errors.New(errString)
}
buffer = make([]byte, 0x200)
buffer[0x00] = byte(blockNumber & 0x00FF)
buffer[0x01] = byte(blockNumber >> 8)
err = WriteBlock(readerWriter, nextBlockNumber, buffer)
if err != nil {
errString := fmt.Sprintf("failed to write new block to expand directory: %s", err)
return 0, errors.New(errString)
}
updateVolumeBitmap(readerWriter, blockList)
buffer, err = ReadBlock(readerWriter, directoryHeader.ParentBlock)
if err != nil {
errString := fmt.Sprintf("failed to read parent block to expand directory: %s", err)
return 0, errors.New(errString)
}
directoryEntryOffset := directoryHeader.ParentEntry*directoryHeader.EntryLength + 0x04
directoryFileEntry := parseFileEntry(buffer[directoryEntryOffset:directoryEntryOffset+0x28], directoryHeader.ParentBlock, directoryHeader.ParentEntry*directoryHeader.EntryLength+0x04)
directoryFileEntry.BlocksUsed++
directoryFileEntry.EndOfFile += 0x200
writeFileEntry(readerWriter, directoryFileEntry)
return nextBlockNumber, nil
}
func getFileEntriesInDirectory(reader io.ReaderAt, blockNumber int, currentPath int, paths []string) (DirectoryHeader, []FileEntry, error) {
buffer, err := ReadBlock(reader, blockNumber)
if err != nil {
@ -333,13 +418,14 @@ func writeFileEntry(writer io.WriterAt, fileEntry FileEntry) {
buffer[0x1E] = byte(fileEntry.Access)
buffer[0x1F] = byte(fileEntry.AuxType & 0x00FF)
buffer[0x20] = byte(fileEntry.AuxType >> 8)
modifiedTime := DateTimeToProDOS(fileEntry.CreationTime)
modifiedTime := DateTimeToProDOS(fileEntry.ModifiedTime)
for i := 0; i < 4; i++ {
buffer[0x21+i] = modifiedTime[i]
}
buffer[0x25] = byte(fileEntry.HeaderPointer & 0x00FF)
buffer[0x26] = byte(fileEntry.HeaderPointer >> 8)
//fmt.Printf("Writing file entry at block: %04X offset: %04X\n", fileEntry.DirectoryBlock, fileEntry.DirectoryOffset)
_, err := writer.WriteAt(buffer, int64(fileEntry.DirectoryBlock*512+fileEntry.DirectoryOffset))
if err != nil {
@ -381,7 +467,8 @@ func parseVolumeHeader(buffer []byte) VolumeHeader {
func parseDirectoryHeader(buffer []byte, blockNumber int) DirectoryHeader {
previousBlock := int(buffer[0x00]) + int(buffer[0x01])*256
nextBlock := int(buffer[0x02]) + int(buffer[0x03])*256
filenameLength := buffer[0x04] & 15
isSubDirectory := (buffer[0x04] & 0xF0) == 0xE0
filenameLength := buffer[0x04] & 0x0F
name := string(buffer[0x05 : filenameLength+0x05])
creationTime := DateTimeFromProDOS(buffer[0x1C:0x20])
version := int(buffer[0x20])
@ -398,6 +485,7 @@ func parseDirectoryHeader(buffer []byte, blockNumber int) DirectoryHeader {
PreviousBlock: previousBlock,
NextBlock: nextBlock,
StartingBlock: blockNumber,
IsSubDirectory: isSubDirectory,
Name: name,
CreationTime: creationTime,
Version: version,
@ -415,6 +503,7 @@ func parseDirectoryHeader(buffer []byte, blockNumber int) DirectoryHeader {
}
func writeDirectoryHeader(readerWriter ReaderWriterAt, directoryHeader DirectoryHeader) error {
// Reading back the block preserves values including reserved fields
buffer, err := ReadBlock(readerWriter, directoryHeader.StartingBlock)
if err != nil {
return err
@ -423,6 +512,11 @@ func writeDirectoryHeader(readerWriter ReaderWriterAt, directoryHeader Directory
buffer[0x01] = byte(directoryHeader.PreviousBlock >> 8)
buffer[0x02] = byte(directoryHeader.NextBlock & 0x00FF)
buffer[0x03] = byte(directoryHeader.NextBlock >> 8)
if directoryHeader.IsSubDirectory {
buffer[0x04] = 0xE0
} else {
buffer[0x04] = 0xF0
}
buffer[0x04] = buffer[0x04] | byte(len(directoryHeader.Name))
for i := 0; i < len(directoryHeader.Name); i++ {
buffer[0x05+i] = directoryHeader.Name[i]
@ -431,6 +525,16 @@ func writeDirectoryHeader(readerWriter ReaderWriterAt, directoryHeader Directory
for i := 0; i < 4; i++ {
buffer[0x1C+i] = creationTime[i]
}
// Without these reserved bytes, reading the directory causes I/O ERROR
buffer[0x14] = 0x75
buffer[0x15] = byte(directoryHeader.Version)
buffer[0x16] = byte(directoryHeader.MinVersion)
buffer[0x17] = 0xC3
buffer[0x18] = 0x0D
buffer[0x19] = 0x27
buffer[0x1A] = 0x00
buffer[0x1B] = 0x00
buffer[0x20] = byte(directoryHeader.Version)
buffer[0x21] = byte(directoryHeader.MinVersion)
buffer[0x22] = byte(directoryHeader.Access)

View File

@ -46,6 +46,10 @@ func LoadFile(reader io.ReaderAt, path string) ([]byte, error) {
func WriteFile(readerWriter ReaderWriterAt, path string, fileType int, auxType int, createdTime time.Time, modifiedTime time.Time, buffer []byte) error {
directory, fileName := GetDirectoryAndFileNameFromPath(path)
if len(fileName) > 15 {
return errors.New("filename too long")
}
existingFileEntry, _ := GetFileEntry(readerWriter, path)
if existingFileEntry.StorageType != StorageDeleted {
DeleteFile(readerWriter, path)
@ -91,6 +95,8 @@ func WriteFile(readerWriter ReaderWriterAt, path string, fileType int, auxType i
fileEntry.EndOfFile = len(buffer)
fileEntry.FileType = fileType
fileEntry.KeyPointer = blockList[0]
fileEntry.Version = 0x24
fileEntry.MinVersion = 0x00
fileEntry.Access = 0b11100011
if len(blockList) == 1 {
fileEntry.StorageType = StorageSeedling
@ -135,6 +141,7 @@ func DeleteFile(readerWriter ReaderWriterAt, path string) error {
if err != nil {
return err
}
volumeBitmap, err := ReadVolumeBitmap(readerWriter)
if err != nil {
return err
@ -307,26 +314,27 @@ func getBlocklist(reader io.ReaderAt, fileEntry FileEntry, dataOnly bool) ([]int
if err != nil {
return nil, err
}
blockOffset := 0
if !dataOnly {
blockOffset := 1
blocks[0] = fileEntry.KeyPointer
blockOffset = 1
}
for i := 0; i < fileEntry.BlocksUsed-1; i++ {
blocks[i+blockOffset] = int(index[i]) + int(index[i+256])*256
}
if dataOnly {
return blocks[1:], nil
}
return blocks, nil
case StorageTree:
// this is actually too large
dataBlocks := make([]int, fileEntry.BlocksUsed)
numberOfIndexBlocks := fileEntry.BlocksUsed/256 + 1
if fileEntry.BlocksUsed%256 != 0 {
numberOfIndexBlocks++
}
// this is also actually too large
numberOfIndexBlocks := fileEntry.BlocksUsed/256 + 2
indexBlocks := make([]int, numberOfIndexBlocks)
masterIndex, err := ReadBlock(reader, fileEntry.KeyPointer)
if err != nil {
return nil, err
}
numberOfDataBlocks := 0
indexBlocks[0] = fileEntry.KeyPointer
indexBlockCount := 1
@ -345,6 +353,7 @@ func getBlocklist(reader io.ReaderAt, fileEntry FileEntry, dataOnly bool) ([]int
if (int(index[j]) + int(index[j+256])*256) == 0 {
break
}
numberOfDataBlocks++
dataBlocks[i*256+j] = int(index[j]) + int(index[j+256])*256
}
}
@ -353,7 +362,7 @@ func getBlocklist(reader io.ReaderAt, fileEntry FileEntry, dataOnly bool) ([]int
return dataBlocks, nil
}
blocks = append(indexBlocks, dataBlocks...)
blocks = append(indexBlocks[0:numberOfIndexBlocks], dataBlocks[0:numberOfDataBlocks]...)
return blocks, nil
}
@ -392,7 +401,7 @@ func createBlockList(reader io.ReaderAt, fileSize int) ([]int, error) {
blockList := findFreeBlocks(volumeBitmap, numberOfBlocks)
return blockList, nil
return blockList[0:numberOfBlocks], nil
}
// GetFileEntry returns a file entry for the given path

View File

@ -9,7 +9,7 @@ import (
"testing"
)
func TestCreatBlocklist(t *testing.T) {
func TestCreateBlocklist(t *testing.T) {
var tests = []struct {
fileSize int
wantBlocks int

View File

@ -8,6 +8,7 @@ package prodos
import (
"encoding/binary"
"errors"
"fmt"
"os"
"path/filepath"
@ -19,7 +20,15 @@ import (
// from the specified host directory
func AddFilesFromHostDirectory(
readerWriter ReaderWriterAt,
directory string) error {
directory string,
path string,
recursive bool) error {
path, err := makeFullPath(path, readerWriter)
if !strings.HasSuffix(path, "/") {
path = path + "/"
}
files, err := os.ReadDir(directory)
if err != nil {
@ -33,37 +42,67 @@ func AddFilesFromHostDirectory(
}
if file.Name()[0] != '.' && !file.IsDir() && info.Size() > 0 && info.Size() <= 0x1000000 {
err = WriteFileFromFile(readerWriter, "", 0, 0, info.ModTime(), filepath.Join(directory, file.Name()))
err = WriteFileFromFile(readerWriter, path, 0, 0, info.ModTime(), filepath.Join(directory, file.Name()))
if err != nil {
return err
}
}
if file.Name()[0] != '.' && recursive && file.IsDir() {
newPath := file.Name()
if len(newPath) > 15 {
newPath = newPath[0:15]
}
newFullPath := strings.ToUpper(path + newPath)
newHostDirectory := filepath.Join(directory, file.Name())
CreateDirectory(readerWriter, newFullPath)
AddFilesFromHostDirectory(readerWriter, newHostDirectory, newFullPath+"/", recursive)
}
}
return nil
}
// WriteFileFromFile writes a file to a ProDOS volume from a host file
func WriteFileFromFile(readerWriter ReaderWriterAt, pathName string, fileType int, auxType int, modifiedTime time.Time, inFileName string) error {
fmt.Printf("WriteFileFromFile: %s\n", inFileName)
func WriteFileFromFile(
readerWriter ReaderWriterAt,
pathName string,
fileType int,
auxType int,
modifiedTime time.Time,
inFileName string) error {
inFile, err := os.ReadFile(inFileName)
if err != nil {
fmt.Println("failed to read file")
return err
errString := fmt.Sprintf("write from file failed: %s", err)
return errors.New(errString)
}
if auxType == 0 && fileType == 0 {
auxType, fileType, inFile, err = convertFileByType(inFileName, inFile)
if err != nil {
fmt.Println("failed to convert file")
return err
errString := fmt.Sprintf("failed to convert file: %s", err)
return errors.New(errString)
}
}
trimExtensions := false
if len(pathName) == 0 {
_, pathName = filepath.Split(inFileName)
pathName = strings.ToUpper(pathName)
trimExtensions = true
}
if strings.HasSuffix(pathName, "/") {
trimExtensions = true
_, fileName := filepath.Split(inFileName)
pathName = strings.ToUpper(pathName + fileName)
}
if trimExtensions {
ext := filepath.Ext(pathName)
if len(ext) > 0 {
switch ext {
case ".SYS", ".TXT", ".BAS", ".BIN":
@ -72,6 +111,13 @@ func WriteFileFromFile(readerWriter ReaderWriterAt, pathName string, fileType in
}
}
paths := strings.SplitAfter(pathName, "/")
if len(paths[len(paths)-1]) > 15 {
paths[len(paths)-1] = paths[len(paths)-1][0:15]
pathName = strings.Join(paths, "")
}
fmt.Printf("Source: %s Destination: %s\n", inFileName, pathName)
return WriteFile(readerWriter, pathName, fileType, auxType, time.Now(), modifiedTime, inFile)
}

View File

@ -100,6 +100,8 @@ func DumpFileEntry(fileEntry FileEntry) {
fmt.Printf("Storage type: %02X\n", fileEntry.StorageType)
fmt.Printf("Header pointer: %04X\n", fileEntry.HeaderPointer)
fmt.Printf("Access: %04X\n", fileEntry.Access)
fmt.Printf("Directory block: %04X\n", fileEntry.DirectoryBlock)
fmt.Printf("Directory offset: %04X\n", fileEntry.DirectoryOffset)
fmt.Printf("\n")
}
@ -119,11 +121,22 @@ func DumpVolumeHeader(volumeHeader VolumeHeader) {
// DumpDirectoryHeader dumps the directory header as text
func DumpDirectoryHeader(directoryHeader DirectoryHeader) {
fmt.Printf("Name: %s\n", directoryHeader.Name)
fmt.Printf("File count: %d\n", directoryHeader.ActiveFileCount)
fmt.Printf("Starting block: %04X\n", directoryHeader.StartingBlock)
fmt.Printf("Previous block: %04X\n", directoryHeader.PreviousBlock)
fmt.Printf("Next block: %04X\n", directoryHeader.NextBlock)
fmt.Printf("Is subdirectory: %t\n", directoryHeader.IsSubDirectory)
fmt.Printf("Name: %s\n", directoryHeader.Name)
fmt.Printf("Creation time: %s\n", TimeToString(directoryHeader.CreationTime))
fmt.Printf("Version: %02X\n", directoryHeader.Version)
fmt.Printf("MinVersion: %02X\n", directoryHeader.MinVersion)
fmt.Printf("Access: %02X\n", directoryHeader.Access)
fmt.Printf("Entry length: %02X\n", directoryHeader.EntryLength)
fmt.Printf("Entries per block: %02X\n", directoryHeader.EntriesPerBlock)
fmt.Printf("File count: %d\n", directoryHeader.ActiveFileCount)
fmt.Printf("Active file count: %04X\n", directoryHeader.ActiveFileCount)
fmt.Printf("Parent block: %04X\n", directoryHeader.ParentBlock)
fmt.Printf("Parent entry: %02X\n", directoryHeader.ParentEntry)
fmt.Printf("Parent entry length: %02X\n", directoryHeader.ParentEntryLength)
}
// DumpBlock dumps the block as hexadecimal and text