Add tree file read support (#16)

* Add tree file read support

* Update copyright year

* Add tree file write support
This commit is contained in:
Terence Boldt 2023-01-03 23:24:48 -05:00 committed by GitHub
parent 0beb2f41fa
commit 6cc7db13e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 243 additions and 163 deletions

View File

@ -8,11 +8,10 @@ Being a work in progress, be warned that this code is likely to corrupt drive im
There are binaries [here](https://github.com/tjboldt/ProDOS-Utilities/releases/latest) There are binaries [here](https://github.com/tjboldt/ProDOS-Utilities/releases/latest)
## Current TODO list ## Current TODO list
1. Allow > 128 KB file support 1. Create/Delete directories
2. Create/Delete directories 2. Add file/directory tests
3. Add file/directory tests 3. Add rename
4. Add rename 4. Add in-place file/directory moves
5. Add in-place file/directory moves
## Example commands and output ## Example commands and output

View File

@ -1,4 +1,4 @@
// Copyright Terence J. Boldt (c)2021-2022 // Copyright Terence J. Boldt (c)2021-2023
// Use of this source code is governed by an MIT // Use of this source code is governed by an MIT
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -17,7 +17,7 @@ import (
"github.com/tjboldt/ProDOS-Utilities/prodos" "github.com/tjboldt/ProDOS-Utilities/prodos"
) )
const version = "0.4.1" const version = "0.4.2"
func main() { func main() {
var fileName string var fileName string

View File

@ -1,4 +1,4 @@
// Copyright Terence J. Boldt (c)2021-2022 // Copyright Terence J. Boldt (c)2021-2023
// Use of this source code is governed by an MIT // Use of this source code is governed by an MIT
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.

View File

@ -1,3 +1,9 @@
// Copyright Terence J. Boldt (c)2022-2023
// Use of this source code is governed by an MIT
// license that can be found in the LICENSE file.
// This file provides tests for conversion between BASIC and text
package prodos package prodos
import ( import (

View File

@ -1,4 +1,4 @@
// Copyright Terence J. Boldt (c)2021-2022 // Copyright Terence J. Boldt (c)2021-2023
// Use of this source code is governed by an MIT // Use of this source code is governed by an MIT
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.

View File

@ -1,3 +1,10 @@
// 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 tests for access to volum bitmap on
// a ProDOS drive image
package prodos package prodos
import ( import (

View File

@ -1,4 +1,4 @@
// Copyright Terence J. Boldt (c)2021-2022 // Copyright Terence J. Boldt (c)2021-2023
// Use of this source code is governed by an MIT // Use of this source code is governed by an MIT
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.

View File

@ -1,4 +1,4 @@
// Copyright Terence J. Boldt (c)2021-2022 // Copyright Terence J. Boldt (c)2021-2023
// Use of this source code is governed by an MIT // Use of this source code is governed by an MIT
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -281,7 +281,7 @@ func parseVolumeHeader(buffer []byte) VolumeHeader {
bitmapBlock := int(buffer[39]) + int(buffer[40])*256 bitmapBlock := int(buffer[39]) + int(buffer[40])*256
totalBlocks := int(buffer[41]) + int(buffer[42])*256 totalBlocks := int(buffer[41]) + int(buffer[42])*256
if version > 0 || minVersion > 0 { if minVersion > 0 {
panic("Unsupported ProDOS version") panic("Unsupported ProDOS version")
} }

View File

@ -1,4 +1,4 @@
// Copyright Terence J. Boldt (c)2021-2022 // Copyright Terence J. Boldt (c)2021-2023
// Use of this source code is governed by an MIT // Use of this source code is governed by an MIT
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.

View File

@ -1,4 +1,4 @@
// Copyright Terence J. Boldt (c)2021-2022 // Copyright Terence J. Boldt (c)2021-2023
// Use of this source code is governed by an MIT // Use of this source code is governed by an MIT
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -8,12 +8,9 @@
package prodos package prodos
import ( import (
"encoding/binary"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"os"
"path/filepath"
"strings" "strings"
"time" "time"
) )
@ -45,93 +42,6 @@ func LoadFile(reader io.ReaderAt, path string) ([]byte, error) {
return buffer, nil return buffer, nil
} }
// WriteFileFromFile writes a file to a ProDOS volume from a host file
func WriteFileFromFile(readerWriter ReaderWriterAt, pathName string, fileType int, auxType int, inFileName string) error {
inFile, err := os.ReadFile(inFileName)
if err != nil {
return err
}
if auxType == 0 && fileType == 0 {
auxType, fileType, inFile, err = convertFileByType(inFileName, inFile)
if err != nil {
return err
}
}
if len(pathName) == 0 {
_, pathName = filepath.Split(inFileName)
pathName = strings.ToUpper(pathName)
ext := filepath.Ext(pathName)
if len(ext) > 0 {
switch ext {
case ".SYS", ".TXT", ".BAS", ".BIN":
pathName = strings.TrimSuffix(pathName, ext)
}
}
}
return WriteFile(readerWriter, pathName, fileType, auxType, inFile)
}
func convertFileByType(inFileName string, inFile []byte) (int, int, []byte, error) {
fileType := 0x06 // default to BIN
auxType := 0x2000 // default to $2000
var err error
// Check for an AppleSingle file as produced by cc65
if // Magic number
binary.BigEndian.Uint32(inFile[0x00:]) == 0x00051600 &&
// Version number
binary.BigEndian.Uint32(inFile[0x04:]) == 0x00020000 &&
// Number of entries
binary.BigEndian.Uint16(inFile[0x18:]) == 0x0002 &&
// Data Fork ID
binary.BigEndian.Uint32(inFile[0x1A:]) == 0x00000001 &&
// Offset
binary.BigEndian.Uint32(inFile[0x1E:]) == 0x0000003A &&
// Length
binary.BigEndian.Uint32(inFile[0x22:]) == uint32(len(inFile))-0x3A &&
// ProDOS File Info ID
binary.BigEndian.Uint32(inFile[0x26:]) == 0x0000000B &&
// Offset
binary.BigEndian.Uint32(inFile[0x2A:]) == 0x00000032 &&
// Length
binary.BigEndian.Uint32(inFile[0x2E:]) == 0x00000008 {
fileType = int(binary.BigEndian.Uint16(inFile[0x34:]))
auxType = int(binary.BigEndian.Uint32(inFile[0x36:]))
inFile = inFile[0x3A:]
} else {
// use extension to determine file type
ext := strings.ToUpper(filepath.Ext(inFileName))
switch ext {
case ".BAS":
inFile, err = ConvertTextToBasic(string(inFile))
fileType = 0xFC
auxType = 0x0801
if err != nil {
return 0, 0, nil, err
}
case ".SYS":
fileType = 0xFF
auxType = 0x2000
case ".BIN":
fileType = 0x06
auxType = 0x2000
case ".TXT":
inFile = []byte(strings.ReplaceAll(strings.ReplaceAll(string(inFile), "\r\n", "r"), "\n", "\r"))
fileType = 0x04
auxType = 0x0000
}
}
return auxType, fileType, inFile, err
}
// WriteFile writes a file to a ProDOS volume from a byte array // WriteFile writes a file to a ProDOS volume from a byte array
func WriteFile(readerWriter ReaderWriterAt, path string, fileType int, auxType int, buffer []byte) error { func WriteFile(readerWriter ReaderWriterAt, path string, fileType int, auxType int, buffer []byte) error {
directory, fileName := GetDirectoryAndFileNameFromPath(path) directory, fileName := GetDirectoryAndFileNameFromPath(path)
@ -149,7 +59,7 @@ func WriteFile(readerWriter ReaderWriterAt, path string, fileType int, auxType i
// seedling file // seedling file
if len(buffer) <= 0x200 { if len(buffer) <= 0x200 {
WriteBlock(readerWriter, blockList[0], buffer) writeSeedlingFile(readerWriter, buffer, blockList)
} }
// sapling file needs index block // sapling file needs index block
@ -218,7 +128,7 @@ func DeleteFile(readerWriter ReaderWriterAt, path string) error {
} }
// free the blocks // free the blocks
blocks, err := getBlocklist(readerWriter, fileEntry) blocks, err := getAllBlockList(readerWriter, fileEntry)
if err != nil { if err != nil {
return err return err
} }
@ -280,6 +190,10 @@ func updateVolumeBitmap(readerWriter ReaderWriterAt, blockList []int) error {
return writeVolumeBitmap(readerWriter, volumeBitmap) return writeVolumeBitmap(readerWriter, volumeBitmap)
} }
func writeSeedlingFile(writer io.WriterAt, buffer []byte, blockList []int) {
WriteBlock(writer, blockList[0], buffer)
}
func writeSaplingFile(writer io.WriterAt, buffer []byte, blockList []int) { func writeSaplingFile(writer io.WriterAt, buffer []byte, blockList []int) {
// write index block with pointers to data blocks // write index block with pointers to data blocks
indexBuffer := make([]byte, 512) indexBuffer := make([]byte, 512)
@ -287,6 +201,9 @@ func writeSaplingFile(writer io.WriterAt, buffer []byte, blockList []int) {
if i < len(blockList)-1 { if i < len(blockList)-1 {
indexBuffer[i] = byte(blockList[i+1] & 0x00FF) indexBuffer[i] = byte(blockList[i+1] & 0x00FF)
indexBuffer[i+256] = byte(blockList[i+1] >> 8) indexBuffer[i+256] = byte(blockList[i+1] >> 8)
} else {
indexBuffer[i] = 0
indexBuffer[i+256] = 0
} }
} }
WriteBlock(writer, blockList[0], indexBuffer) WriteBlock(writer, blockList[0], indexBuffer)
@ -313,11 +230,69 @@ func writeSaplingFile(writer io.WriterAt, buffer []byte, blockList []int) {
} }
func writeTreeFile(writer io.WriterAt, buffer []byte, blockList []int) { func writeTreeFile(writer io.WriterAt, buffer []byte, blockList []int) {
// write master index block with pointers to index blocks
indexBuffer := make([]byte, 512)
numberOfIndexBlocks := len(blockList)/256 + 1
if len(blockList)%256 == 0 {
numberOfIndexBlocks--
}
for i := 0; i < 256; i++ {
if i < numberOfIndexBlocks {
indexBuffer[i] = byte(blockList[i+1] & 0x00FF)
indexBuffer[i+256] = byte(blockList[i+1] >> 8)
} else {
indexBuffer[i] = 0
indexBuffer[i+256] = 0
}
}
WriteBlock(writer, blockList[0], indexBuffer)
numberOfIndexBlocks++
// write index blocks
for i := 0; i < len(blockList)/256+1; i++ {
for j := 0; j < 256; j++ {
if i*256+j < len(blockList)-numberOfIndexBlocks {
indexBuffer[j] = byte(blockList[i*256+numberOfIndexBlocks+j] & 0x00FF)
indexBuffer[j+256] = byte(blockList[i*256+j+2] >> 8)
} else {
indexBuffer[j] = 0
indexBuffer[j+256] = 0
}
}
WriteBlock(writer, blockList[i+1], indexBuffer)
}
// write all data blocks
blockBuffer := make([]byte, 512)
blockPointer := 0
blockIndexNumber := numberOfIndexBlocks
for i := 0; i < len(buffer); i++ {
blockBuffer[blockPointer] = buffer[i]
if blockPointer == 511 {
WriteBlock(writer, blockList[blockIndexNumber], blockBuffer)
blockPointer = 0
blockIndexNumber++
} else if i == len(buffer)-1 {
for j := blockPointer; j < 512; j++ {
blockBuffer[j] = 0
}
WriteBlock(writer, blockList[blockIndexNumber], blockBuffer)
} else {
blockPointer++
}
}
}
func getDataBlocklist(reader io.ReaderAt, fileEntry FileEntry) ([]int, error) {
return getBlocklist(reader, fileEntry, true)
}
func getAllBlockList(reader io.ReaderAt, fileEntry FileEntry) ([]int, error) {
return getBlocklist(reader, fileEntry, false)
} }
// Returns all blocks, including index blocks // Returns all blocks, including index blocks
func getBlocklist(reader io.ReaderAt, fileEntry FileEntry) ([]int, error) { func getBlocklist(reader io.ReaderAt, fileEntry FileEntry, dataOnly bool) ([]int, error) {
blocks := make([]int, fileEntry.BlocksUsed) blocks := make([]int, fileEntry.BlocksUsed)
switch fileEntry.StorageType { switch fileEntry.StorageType {
@ -329,58 +304,54 @@ func getBlocklist(reader io.ReaderAt, fileEntry FileEntry) ([]int, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
blockOffset := 0
if !dataOnly {
blocks[0] = fileEntry.KeyPointer blocks[0] = fileEntry.KeyPointer
blockOffset = 1
}
for i := 0; i < fileEntry.BlocksUsed-1; i++ { for i := 0; i < fileEntry.BlocksUsed-1; i++ {
blocks[i+1] = int(index[i]) + int(index[i+256])*256 blocks[i+blockOffset] = int(index[i]) + int(index[i+256])*256
} }
return blocks, nil return blocks, nil
case StorageTree: case StorageTree:
dataBlocks := make([]int, fileEntry.BlocksUsed)
numberOfIndexBlocks := fileEntry.BlocksUsed/256 + 1
if fileEntry.BlocksUsed%256 != 0 {
numberOfIndexBlocks++
}
indexBlocks := make([]int, numberOfIndexBlocks)
masterIndex, err := ReadBlock(reader, fileEntry.KeyPointer) masterIndex, err := ReadBlock(reader, fileEntry.KeyPointer)
if err != nil { if err != nil {
return nil, err return nil, err
} }
blocks[0] = fileEntry.KeyPointer indexBlocks[0] = fileEntry.KeyPointer
indexBlockCount := 1
for i := 0; i < 128; i++ { for i := 0; i < 128; i++ {
index, err := ReadBlock(reader, int(masterIndex[i])+int(masterIndex[i+256])*256) indexBlock := int(masterIndex[i]) + int(masterIndex[i+256])*256
if indexBlock == 0 {
break
}
indexBlocks[indexBlockCount] = indexBlock
indexBlockCount++
index, err := ReadBlock(reader, indexBlock)
if err != nil { if err != nil {
return nil, err return nil, err
} }
for j := 0; j < 256 && i*256+j < fileEntry.BlocksUsed; j++ { for j := 0; j < 256 && i*256+j < fileEntry.BlocksUsed; j++ {
if (int(index[j]) + int(index[j+256])*256) == 0 { if (int(index[j]) + int(index[j+256])*256) == 0 {
return blocks, nil break
}
blocks[i*256+j] = int(index[j]) + int(index[j+256])*256
} }
dataBlocks[i*256+j] = int(index[j]) + int(index[j+256])*256
} }
} }
return nil, errors.New("unsupported file storage type") if dataOnly {
return dataBlocks, nil
} }
func getDataBlocklist(reader io.ReaderAt, fileEntry FileEntry) ([]int, error) { blocks = append(indexBlocks, dataBlocks...)
switch fileEntry.StorageType {
case StorageSeedling:
blocks := make([]int, 1)
blocks[0] = fileEntry.KeyPointer
return blocks, nil return blocks, nil
case StorageSapling:
blocks := make([]int, fileEntry.BlocksUsed-1)
index, err := ReadBlock(reader, fileEntry.KeyPointer)
if err != nil {
return nil, err
}
for i := 0; i < fileEntry.BlocksUsed-1; i++ {
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")
@ -388,20 +359,16 @@ func getDataBlocklist(reader io.ReaderAt, fileEntry FileEntry) ([]int, error) {
func createBlockList(reader io.ReaderAt, fileSize int) ([]int, error) { func createBlockList(reader io.ReaderAt, fileSize int) ([]int, error) {
numberOfBlocks := fileSize / 512 numberOfBlocks := fileSize / 512
//fmt.Printf("Number of blocks %d\n", numberOfBlocks)
if fileSize%512 > 0 { if fileSize%512 > 0 {
//fmt.Printf("Adding block for partial usage\n")
numberOfBlocks++ numberOfBlocks++
} }
if fileSize > 0x200 && fileSize <= 0x20000 { if fileSize > 0x200 && fileSize <= 0x20000 {
//fmt.Printf("Adding index block for sapling file\n")
numberOfBlocks++ // add index block numberOfBlocks++ // add index block
} }
if fileSize > 0x20000 && fileSize <= 0x1000000 { if fileSize > 0x20000 && fileSize <= 0x1000000 {
//fmt.Printf("Tree file\n")
// add index blocks for each 256 blocks // add index blocks for each 256 blocks
numberOfBlocks += numberOfBlocks / 256 numberOfBlocks += numberOfBlocks / 256
// add index block for any remaining blocks // add index block for any remaining blocks
@ -420,7 +387,6 @@ func createBlockList(reader io.ReaderAt, fileSize int) ([]int, error) {
return nil, err return nil, err
} }
//fmt.Printf("findFreeBlocks %d\n", numberOfBlocks)
blockList := findFreeBlocks(volumeBitmap, numberOfBlocks) blockList := findFreeBlocks(volumeBitmap, numberOfBlocks)
return blockList, nil return blockList, nil

View File

@ -1,4 +1,4 @@
// Copyright Terence J. Boldt (c)2021-2022 // Copyright Terence J. Boldt (c)2021-2023
// Use of this source code is governed by an MIT // Use of this source code is governed by an MIT
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.

View File

@ -1,4 +1,4 @@
// Copyright Terence J. Boldt (c)2021-2022 // Copyright Terence J. Boldt (c)2021-2023
// Use of this source code is governed by an MIT // Use of this source code is governed by an MIT
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.

View File

@ -1,3 +1,9 @@
// 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 tests for access to format a ProDOS drive image
package prodos package prodos
import ( import (

View File

@ -1,4 +1,4 @@
// Copyright Terence J. Boldt (c)2022 // Copyright Terence J. Boldt (c)2022-2023
// Use of this source code is governed by an MIT // Use of this source code is governed by an MIT
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -7,8 +7,10 @@
package prodos package prodos
import ( import (
"encoding/binary"
"os" "os"
"path/filepath" "path/filepath"
"strings"
) )
// AddFilesFromHostDirectory fills the root volume with files // AddFilesFromHostDirectory fills the root volume with files
@ -28,7 +30,7 @@ func AddFilesFromHostDirectory(
return err return err
} }
if !file.IsDir() && info.Size() > 0 && info.Size() <= 0x20000 { if !file.IsDir() && info.Size() > 0 && info.Size() <= 0x1000000 {
err = WriteFileFromFile(readerWriter, "", 0, 0, filepath.Join(directory, file.Name())) err = WriteFileFromFile(readerWriter, "", 0, 0, filepath.Join(directory, file.Name()))
if err != nil { if err != nil {
return err return err
@ -38,3 +40,90 @@ func AddFilesFromHostDirectory(
return nil return nil
} }
// WriteFileFromFile writes a file to a ProDOS volume from a host file
func WriteFileFromFile(readerWriter ReaderWriterAt, pathName string, fileType int, auxType int, inFileName string) error {
inFile, err := os.ReadFile(inFileName)
if err != nil {
return err
}
if auxType == 0 && fileType == 0 {
auxType, fileType, inFile, err = convertFileByType(inFileName, inFile)
if err != nil {
return err
}
}
if len(pathName) == 0 {
_, pathName = filepath.Split(inFileName)
pathName = strings.ToUpper(pathName)
ext := filepath.Ext(pathName)
if len(ext) > 0 {
switch ext {
case ".SYS", ".TXT", ".BAS", ".BIN":
pathName = strings.TrimSuffix(pathName, ext)
}
}
}
return WriteFile(readerWriter, pathName, fileType, auxType, inFile)
}
func convertFileByType(inFileName string, inFile []byte) (int, int, []byte, error) {
fileType := 0x06 // default to BIN
auxType := 0x2000 // default to $2000
var err error
// Check for an AppleSingle file as produced by cc65
if // Magic number
binary.BigEndian.Uint32(inFile[0x00:]) == 0x00051600 &&
// Version number
binary.BigEndian.Uint32(inFile[0x04:]) == 0x00020000 &&
// Number of entries
binary.BigEndian.Uint16(inFile[0x18:]) == 0x0002 &&
// Data Fork ID
binary.BigEndian.Uint32(inFile[0x1A:]) == 0x00000001 &&
// Offset
binary.BigEndian.Uint32(inFile[0x1E:]) == 0x0000003A &&
// Length
binary.BigEndian.Uint32(inFile[0x22:]) == uint32(len(inFile))-0x3A &&
// ProDOS File Info ID
binary.BigEndian.Uint32(inFile[0x26:]) == 0x0000000B &&
// Offset
binary.BigEndian.Uint32(inFile[0x2A:]) == 0x00000032 &&
// Length
binary.BigEndian.Uint32(inFile[0x2E:]) == 0x00000008 {
fileType = int(binary.BigEndian.Uint16(inFile[0x34:]))
auxType = int(binary.BigEndian.Uint32(inFile[0x36:]))
inFile = inFile[0x3A:]
} else {
// use extension to determine file type
ext := strings.ToUpper(filepath.Ext(inFileName))
switch ext {
case ".BAS":
inFile, err = ConvertTextToBasic(string(inFile))
fileType = 0xFC
auxType = 0x0801
if err != nil {
return 0, 0, nil, err
}
case ".SYS":
fileType = 0xFF
auxType = 0x2000
case ".BIN":
fileType = 0x06
auxType = 0x2000
case ".TXT":
inFile = []byte(strings.ReplaceAll(strings.ReplaceAll(string(inFile), "\r\n", "r"), "\n", "\r"))
fileType = 0x04
auxType = 0x0000
}
}
return auxType, fileType, inFile, err
}

View File

@ -1,4 +1,4 @@
// Copyright Terence J. Boldt (c)2021-2022 // Copyright Terence J. Boldt (c)2021-2023
// Use of this source code is governed by an MIT // Use of this source code is governed by an MIT
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.

View File

@ -1,4 +1,4 @@
// Copyright Terence J. Boldt (c)2021-2022 // Copyright Terence J. Boldt (c)2021-2023
// Use of this source code is governed by an MIT // Use of this source code is governed by an MIT
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.

View File

@ -1,4 +1,4 @@
// Copyright Terence J. Boldt (c)2021-2022 // Copyright Terence J. Boldt (c)2021-2023
// Use of this source code is governed by an MIT // Use of this source code is governed by an MIT
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -11,6 +11,7 @@ import (
) )
// DateTimeToProDOS converts Time to ProDOS date time // 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 // 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0

View File

@ -1,3 +1,9 @@
// 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 tests for conversion to and from ProDOS time format
package prodos package prodos
import ( import (

View File

@ -1,4 +1,4 @@
// Copyright Terence J. Boldt (c)2021-2022 // Copyright Terence J. Boldt (c)2021-2023
// Use of this source code is governed by an MIT // Use of this source code is governed by an MIT
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -14,7 +14,7 @@ func printReadme() {
fmt.Println(` fmt.Println(`
MIT License MIT License
Copyright (c)2021-2022 Terence Boldt Copyright (c)2021-2023 Terence Boldt
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal