mirror of
https://github.com/tjboldt/ProDOS-Utilities.git
synced 2024-12-01 01:49:59 +00:00
554 lines
17 KiB
Go
554 lines
17 KiB
Go
// Copyright Terence J. Boldt (c)2021-2023
|
|
// Use of this source code is governed by an MIT
|
|
// license that can be found in the LICENSE file.
|
|
|
|
// This file provides access to read, write, delete
|
|
// fand parse directories on a ProDOS drive image
|
|
|
|
package prodos
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// VolumeHeader from ProDOS
|
|
type VolumeHeader struct {
|
|
VolumeName string
|
|
CreationTime time.Time
|
|
ActiveFileCount int
|
|
BitmapStartBlock int
|
|
TotalBlocks int
|
|
NextBlock int
|
|
EntryLength int
|
|
EntriesPerBlock int
|
|
MinVersion int
|
|
Version int
|
|
}
|
|
|
|
// DirectoryHeader from ProDOS
|
|
type DirectoryHeader struct {
|
|
PreviousBlock int
|
|
NextBlock int
|
|
IsSubDirectory bool
|
|
Name string
|
|
CreationTime time.Time
|
|
Version int
|
|
MinVersion int
|
|
Access int
|
|
EntryLength int
|
|
EntriesPerBlock int
|
|
ActiveFileCount int
|
|
StartingBlock int
|
|
ParentBlock int
|
|
ParentEntry int
|
|
ParentEntryLength int
|
|
}
|
|
|
|
const (
|
|
// StorageDeleted signifies file is deleted
|
|
StorageDeleted = 0
|
|
// StorageSeedling signifies file is <= 512 bytes
|
|
StorageSeedling = 1
|
|
// StorageSapling signifies file is > 512 bytes and <= 128 KB
|
|
StorageSapling = 2
|
|
// StorageTree signifies file is > 128 KB and <= 16 MB
|
|
StorageTree = 3
|
|
// StoragePascal signifies pascal storage area
|
|
StoragePascal = 4
|
|
// StorageDirectory signifies directory
|
|
StorageDirectory = 13
|
|
)
|
|
|
|
// FileEntry from ProDOS
|
|
type FileEntry struct {
|
|
StorageType int
|
|
FileName string
|
|
FileType int
|
|
CreationTime time.Time
|
|
KeyPointer int
|
|
Version int
|
|
MinVersion int
|
|
BlocksUsed int
|
|
EndOfFile int
|
|
Access int
|
|
AuxType int
|
|
ModifiedTime time.Time
|
|
HeaderPointer int
|
|
|
|
DirectoryBlock int
|
|
DirectoryOffset int
|
|
}
|
|
|
|
// ReadDirectory reads the directory information from a specified path
|
|
// on a ProDOS image
|
|
func ReadDirectory(reader io.ReaderAt, path string) (VolumeHeader, DirectoryHeader, []FileEntry, error) {
|
|
buffer, err := ReadBlock(reader, 2)
|
|
if err != nil {
|
|
return VolumeHeader{}, DirectoryHeader{}, nil, err
|
|
}
|
|
|
|
volumeHeader := parseVolumeHeader(buffer)
|
|
|
|
if len(path) == 0 {
|
|
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, "/")
|
|
|
|
directoryHeader, fileEntries, err := getFileEntriesInDirectory(reader, 2, 1, paths)
|
|
if err != nil {
|
|
return VolumeHeader{}, DirectoryHeader{}, nil, err
|
|
}
|
|
|
|
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 {
|
|
return errors.New("directory already exists")
|
|
}
|
|
|
|
fileEntry, err := getFreeFileEntryInDirectory(readerWriter, parentPath)
|
|
if err != nil {
|
|
errString := fmt.Sprintf("failed to create directory: %s", err)
|
|
return errors.New(errString)
|
|
}
|
|
|
|
// get list of blocks to write file to
|
|
blockList, err := createBlockList(readerWriter, 512)
|
|
if err != nil {
|
|
errString := fmt.Sprintf("failed to create directory: %s", err)
|
|
return errors.New(errString)
|
|
}
|
|
|
|
updateVolumeBitmap(readerWriter, blockList)
|
|
|
|
fileEntry.FileName = newDirectory
|
|
fileEntry.BlocksUsed = 1
|
|
fileEntry.CreationTime = time.Now()
|
|
fileEntry.ModifiedTime = time.Now()
|
|
fileEntry.AuxType = 0
|
|
fileEntry.EndOfFile = 0x200
|
|
fileEntry.FileType = 0x0F
|
|
fileEntry.KeyPointer = blockList[0]
|
|
fileEntry.Access = 0b11100011
|
|
fileEntry.StorageType = StorageDirectory
|
|
fileEntry.Version = 0x24
|
|
fileEntry.MinVersion = 0x00
|
|
|
|
writeFileEntry(readerWriter, fileEntry)
|
|
|
|
err = incrementFileCount(readerWriter, fileEntry)
|
|
if err != nil {
|
|
errString := fmt.Sprintf("failed to create directory: %s", err)
|
|
return errors.New(errString)
|
|
}
|
|
|
|
directoryEntry := DirectoryHeader{
|
|
PreviousBlock: 0,
|
|
NextBlock: 0,
|
|
IsSubDirectory: true,
|
|
Name: newDirectory,
|
|
CreationTime: time.Now(),
|
|
Version: 0x24,
|
|
MinVersion: 0,
|
|
Access: 0xE3,
|
|
EntryLength: 0x27,
|
|
EntriesPerBlock: 0x0D,
|
|
ActiveFileCount: 0,
|
|
StartingBlock: blockList[0],
|
|
ParentBlock: fileEntry.DirectoryBlock,
|
|
ParentEntry: (fileEntry.DirectoryOffset - 0x04) / 0x27,
|
|
ParentEntryLength: 0x27,
|
|
}
|
|
|
|
err = writeDirectoryHeader(readerWriter, directoryEntry)
|
|
if err != nil {
|
|
errString := fmt.Sprintf("failed to create directory: %s", err)
|
|
return errors.New(errString)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
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
|
|
}
|
|
blockNumber := directoryHeader.StartingBlock
|
|
buffer, err := ReadBlock(readerWriter, blockNumber)
|
|
if err != nil {
|
|
return FileEntry{}, err
|
|
}
|
|
entryOffset := 43 // start at offset after header
|
|
entryNumber := 2 // header is essentially the first entry so start at 2
|
|
|
|
for {
|
|
if entryNumber > 13 {
|
|
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(readerWriter, blockNumber)
|
|
if err != nil {
|
|
return FileEntry{}, nil
|
|
}
|
|
|
|
entryOffset = 4
|
|
entryNumber = 1
|
|
}
|
|
fileEntry := parseFileEntry(buffer[entryOffset:entryOffset+0x28], blockNumber, entryOffset)
|
|
|
|
if fileEntry.StorageType == StorageDeleted {
|
|
fileEntry = FileEntry{}
|
|
fileEntry.DirectoryBlock = blockNumber
|
|
fileEntry.DirectoryOffset = entryOffset
|
|
fileEntry.HeaderPointer = directoryHeader.StartingBlock
|
|
return fileEntry, nil
|
|
}
|
|
|
|
entryNumber++
|
|
entryOffset += 39
|
|
}
|
|
}
|
|
|
|
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 {
|
|
return DirectoryHeader{}, nil, err
|
|
}
|
|
|
|
directoryHeader := parseDirectoryHeader(buffer, blockNumber)
|
|
|
|
fileEntries := make([]FileEntry, directoryHeader.ActiveFileCount)
|
|
entryOffset := 43 // start at offset after header
|
|
activeEntries := 0
|
|
entryNumber := 2 // header is essentially the first entry so start at 2
|
|
|
|
nextBlock := directoryHeader.NextBlock
|
|
|
|
matchedDirectory := (currentPath == len(paths)-1) && (paths[currentPath] == directoryHeader.Name)
|
|
|
|
if !matchedDirectory && (currentPath == len(paths)-1) {
|
|
// path not matched by last path part
|
|
return DirectoryHeader{}, nil, errors.New("path not matched")
|
|
}
|
|
|
|
for {
|
|
if entryNumber > 13 {
|
|
entryOffset = 4
|
|
entryNumber = 1
|
|
if blockNumber == 0 {
|
|
return DirectoryHeader{}, nil, nil
|
|
}
|
|
buffer, err = ReadBlock(reader, nextBlock)
|
|
if err != nil {
|
|
return DirectoryHeader{}, nil, err
|
|
}
|
|
nextBlock = int(buffer[2]) + int(buffer[3])*256
|
|
}
|
|
fileEntry := parseFileEntry(buffer[entryOffset:entryOffset+40], blockNumber, entryOffset)
|
|
|
|
if fileEntry.StorageType != StorageDeleted {
|
|
if matchedDirectory && activeEntries == directoryHeader.ActiveFileCount {
|
|
return directoryHeader, fileEntries[0:activeEntries], nil
|
|
}
|
|
if matchedDirectory {
|
|
fileEntries[activeEntries] = fileEntry
|
|
} else if !matchedDirectory && fileEntry.FileType == 15 && paths[currentPath+1] == fileEntry.FileName {
|
|
return getFileEntriesInDirectory(reader, fileEntry.KeyPointer, currentPath+1, paths)
|
|
}
|
|
activeEntries++
|
|
}
|
|
|
|
entryNumber++
|
|
entryOffset += 39
|
|
}
|
|
}
|
|
|
|
func parseFileEntry(buffer []byte, blockNumber int, entryOffset int) FileEntry {
|
|
storageType := int(buffer[0] >> 4)
|
|
fileNameLength := int(buffer[0] & 15)
|
|
fileName := string(buffer[1 : fileNameLength+1])
|
|
fileType := int(buffer[16])
|
|
startingBlock := int(buffer[17]) + int(buffer[18])*256
|
|
blocksUsed := int(buffer[19]) + int(buffer[20])*256
|
|
endOfFile := int(buffer[21]) + int(buffer[22])*256 + int(buffer[23])*65536
|
|
creationTime := DateTimeFromProDOS(buffer[24:28])
|
|
version := int(buffer[28])
|
|
minVersion := int(buffer[29])
|
|
access := int(buffer[30])
|
|
auxType := int(buffer[31]) + int(buffer[32])*256
|
|
modifiedTime := DateTimeFromProDOS((buffer[33:37]))
|
|
headerPointer := int(buffer[0x25]) + int(buffer[0x26])*256
|
|
|
|
fileEntry := FileEntry{
|
|
StorageType: storageType,
|
|
FileName: fileName,
|
|
FileType: fileType,
|
|
CreationTime: creationTime,
|
|
Version: version,
|
|
MinVersion: minVersion,
|
|
KeyPointer: startingBlock,
|
|
BlocksUsed: blocksUsed,
|
|
EndOfFile: endOfFile,
|
|
Access: access,
|
|
AuxType: auxType,
|
|
ModifiedTime: modifiedTime,
|
|
HeaderPointer: headerPointer,
|
|
DirectoryBlock: blockNumber,
|
|
DirectoryOffset: entryOffset,
|
|
}
|
|
|
|
return fileEntry
|
|
}
|
|
|
|
func writeFileEntry(writer io.WriterAt, fileEntry FileEntry) {
|
|
buffer := make([]byte, 39)
|
|
buffer[0] = byte(fileEntry.StorageType)<<4 + byte(len(fileEntry.FileName))
|
|
for i := 0; i < len(fileEntry.FileName); i++ {
|
|
buffer[i+1] = fileEntry.FileName[i]
|
|
}
|
|
buffer[0x10] = byte(fileEntry.FileType)
|
|
buffer[0x11] = byte(fileEntry.KeyPointer & 0xFF)
|
|
buffer[0x12] = byte(fileEntry.KeyPointer >> 8)
|
|
buffer[0x13] = byte(fileEntry.BlocksUsed & 0xFF)
|
|
buffer[0x14] = byte(fileEntry.BlocksUsed >> 8)
|
|
buffer[0x15] = byte(fileEntry.EndOfFile & 0x0000FF)
|
|
buffer[0x16] = byte(fileEntry.EndOfFile & 0x00FF00 >> 8)
|
|
buffer[0x17] = byte(fileEntry.EndOfFile & 0xFF0000 >> 16)
|
|
creationTime := DateTimeToProDOS(fileEntry.CreationTime)
|
|
for i := 0; i < 4; i++ {
|
|
buffer[0x18+i] = creationTime[i]
|
|
}
|
|
buffer[0x1C] = byte(fileEntry.Version)
|
|
buffer[0x1D] = byte(fileEntry.MinVersion)
|
|
buffer[0x1E] = byte(fileEntry.Access)
|
|
buffer[0x1F] = byte(fileEntry.AuxType & 0x00FF)
|
|
buffer[0x20] = byte(fileEntry.AuxType >> 8)
|
|
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 {
|
|
|
|
}
|
|
}
|
|
|
|
func parseVolumeHeader(buffer []byte) VolumeHeader {
|
|
nextBlock := int(buffer[2]) + int(buffer[3])*256
|
|
filenameLength := buffer[4] & 15
|
|
volumeName := string(buffer[5 : filenameLength+5])
|
|
creationTime := DateTimeFromProDOS(buffer[28:32])
|
|
version := int(buffer[32])
|
|
minVersion := int(buffer[33])
|
|
entryLength := int(buffer[35])
|
|
entriesPerBlock := int(buffer[36])
|
|
fileCount := int(buffer[37]) + int(buffer[38])*256
|
|
bitmapBlock := int(buffer[39]) + int(buffer[40])*256
|
|
totalBlocks := int(buffer[41]) + int(buffer[42])*256
|
|
|
|
if minVersion > 0 {
|
|
panic("Unsupported ProDOS version")
|
|
}
|
|
|
|
volumeHeader := VolumeHeader{
|
|
VolumeName: volumeName,
|
|
CreationTime: creationTime,
|
|
ActiveFileCount: fileCount,
|
|
BitmapStartBlock: bitmapBlock,
|
|
TotalBlocks: totalBlocks,
|
|
NextBlock: nextBlock,
|
|
EntriesPerBlock: entriesPerBlock,
|
|
EntryLength: entryLength,
|
|
MinVersion: minVersion,
|
|
Version: version,
|
|
}
|
|
return volumeHeader
|
|
}
|
|
|
|
func parseDirectoryHeader(buffer []byte, blockNumber int) DirectoryHeader {
|
|
previousBlock := int(buffer[0x00]) + int(buffer[0x01])*256
|
|
nextBlock := int(buffer[0x02]) + int(buffer[0x03])*256
|
|
isSubDirectory := (buffer[0x04] & 0xF0) == 0xE0
|
|
filenameLength := buffer[0x04] & 0x0F
|
|
name := string(buffer[0x05 : filenameLength+0x05])
|
|
creationTime := DateTimeFromProDOS(buffer[0x1C:0x20])
|
|
version := int(buffer[0x20])
|
|
minVersion := int(buffer[0x21])
|
|
access := int(buffer[0x22])
|
|
entryLength := int(buffer[0x23])
|
|
entriesPerBlock := int(buffer[0x24])
|
|
fileCount := int(buffer[0x25]) + int(buffer[0x26])*256
|
|
parentBlock := int(buffer[0x27]) + int(buffer[0x28])*256
|
|
parentEntry := int(buffer[0x29])
|
|
parentEntryLength := int(buffer[0x2A])
|
|
|
|
directoryEntry := DirectoryHeader{
|
|
PreviousBlock: previousBlock,
|
|
NextBlock: nextBlock,
|
|
StartingBlock: blockNumber,
|
|
IsSubDirectory: isSubDirectory,
|
|
Name: name,
|
|
CreationTime: creationTime,
|
|
Version: version,
|
|
MinVersion: minVersion,
|
|
Access: access,
|
|
EntryLength: entryLength,
|
|
EntriesPerBlock: entriesPerBlock,
|
|
ActiveFileCount: fileCount,
|
|
ParentBlock: parentBlock,
|
|
ParentEntry: parentEntry,
|
|
ParentEntryLength: parentEntryLength,
|
|
}
|
|
|
|
return directoryEntry
|
|
}
|
|
|
|
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
|
|
}
|
|
buffer[0x00] = byte(directoryHeader.PreviousBlock & 0x00FF)
|
|
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]
|
|
}
|
|
creationTime := DateTimeToProDOS(directoryHeader.CreationTime)
|
|
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)
|
|
buffer[0x23] = byte(directoryHeader.EntryLength)
|
|
buffer[0x24] = byte(directoryHeader.EntriesPerBlock)
|
|
buffer[0x25] = byte(directoryHeader.ActiveFileCount & 0x00FF)
|
|
buffer[0x26] = byte(directoryHeader.ActiveFileCount >> 8)
|
|
buffer[0x27] = byte(directoryHeader.ParentBlock & 0x00FF)
|
|
buffer[0x28] = byte(directoryHeader.ParentBlock >> 8)
|
|
buffer[0x29] = byte(directoryHeader.ParentEntry)
|
|
buffer[0x2A] = byte(directoryHeader.ParentEntryLength)
|
|
WriteBlock(readerWriter, directoryHeader.StartingBlock, buffer)
|
|
|
|
return nil
|
|
}
|