diskm8/shell.go
2018-07-18 20:39:11 -05:00

1864 lines
40 KiB
Go

package main
import (
"bufio"
"fmt"
"io/ioutil"
"runtime/debug"
"strings"
"time"
"path/filepath"
"sort"
"os"
"regexp"
"strconv"
"errors"
"github.com/chzyer/readline"
"github.com/paleotronic/diskm8/disk"
"github.com/paleotronic/diskm8/loggy"
"github.com/paleotronic/diskm8/panic"
)
const MAXVOL = 8
var commandList map[string]*shellCommand
var commandVolumes [MAXVOL]*disk.DSKWrapper
var commandTarget int = -1
var commandPath [MAXVOL]string
func mountDsk(dsk *disk.DSKWrapper) (int, error) {
var fr []int
for i, d := range commandVolumes {
if d == nil {
fr = append(fr, i)
} else if dsk.Filename == d.Filename {
return i, nil
}
}
if len(fr) == 0 {
return -1, errors.New("No free slots")
}
commandVolumes[fr[0]] = dsk
return fr[0], nil
}
func smartSplit(line string) (string, []string) {
var out []string
var inqq bool
var lastEscape bool
var chunk string
add := func() {
if chunk != "" {
out = append(out, chunk)
chunk = ""
}
}
for _, ch := range line {
switch {
case ch == '"':
inqq = !inqq
add()
case ch == ' ':
if inqq || lastEscape {
chunk += string(ch)
} else {
add()
}
lastEscape = false
case ch == '\\' && !inqq:
lastEscape = true
default:
chunk += string(ch)
}
}
add()
if len(out) == 0 {
return "", out
}
return out[0], out[1:]
}
func getPrompt(wp [MAXVOL]string, t int) string {
if t == -1 || commandVolumes[t] == nil {
return fmt.Sprintf("dsk:%d:%s:%s> ", 0, "<no mount>", wp)
}
dsk := commandVolumes[t]
if dsk != nil {
return fmt.Sprintf("dsk:%d:%s:%s> ", t, filepath.Base(dsk.Filename), wp[t])
}
return "dsk> "
}
type shellCommand struct {
Name string
Description string
MinArgs, MaxArgs int
Code func(args []string) int
NeedsMount bool
Context shellCommandContext
Text []string
}
type shellCommandContext int
const (
sccNone shellCommandContext = 1 << iota
sccLocal
sccDiskFile
sccCommand
sccReportName
sccAnyFile = sccDiskFile | sccLocal
sccAny = sccAnyFile | sccCommand
)
type shellCompleter struct {
}
func hasPrefix(str []rune, prefix []rune) bool {
if len(prefix) > len(str) {
return false
}
for i := 0; i < len(prefix); i++ {
if str[i] != prefix[i] {
return false
}
}
return true
}
func (sc *shellCompleter) Do(line []rune, pos int) ([][]rune, int) {
prefix := ""
chunk := ""
for _, ch := range line {
if ch == ' ' {
prefix = chunk
break
} else {
chunk += string(ch)
}
}
chunk = ""
cprefix := ""
var lastEscape bool
for i := 0; i < pos; i++ {
ch := line[i]
switch {
case ch == '\\':
lastEscape = true
case ch == ' ' && !lastEscape:
cprefix = chunk
chunk = ""
lastEscape = false
default:
chunk += string(ch)
}
}
if chunk != "" {
cprefix = chunk
}
var context shellCommandContext = sccNone
cmd, match := commandList[prefix]
if match {
context = cmd.Context
} else {
context = sccCommand
}
var items [][]rune
switch context {
case sccCommand:
for k, _ := range commandList {
items = append(items, []rune(k))
}
case sccDiskFile:
if commandTarget == -1 || commandVolumes[commandTarget] == nil {
return [][]rune(nil), 0
}
fullpath, _ := filepath.Abs(commandVolumes[commandTarget].Filename)
info, err := analyze(0, fullpath)
if err != nil {
return [][]rune(nil), 0
}
for _, f := range info.Files {
items = append(items, []rune(f.Filename))
}
case sccLocal:
files, err := filepath.Glob(cprefix + "*")
//fmt.Println(err)
if err != nil {
return items, 0
}
for _, v := range files {
items = append(items, []rune(v))
//fmt.Println("thing:", v)
}
}
if len(items) == 0 {
return [][]rune(nil), 0
}
//fmt.Printf("Context = %d, CPrefix=%s, Items=%v\n", context, cprefix, items)
var filt [][]rune
for _, v := range items {
if hasPrefix(v, []rune(cprefix)) {
filt = append(filt, shellEscape(v[len(cprefix):]))
}
}
return filt, len(cprefix)
}
func shellEscape(str []rune) []rune {
out := make([]rune, 0)
for _, v := range str {
if v == ' ' {
out = append(out, '\\')
}
out = append(out, v)
}
return out
}
func init() {
commandList = map[string]*shellCommand{
"mount": &shellCommand{
Name: "mount",
Description: "Mount a disk image",
MinArgs: 1,
MaxArgs: 1,
Code: shellMount,
NeedsMount: false,
Context: sccLocal,
Text: []string{
"mount <diskfile>",
"",
"Mounts disk and switches to the new slot",
},
},
"setvolume": &shellCommand{
Name: "setvolume",
Description: "Sets the ProDOS volume name",
MinArgs: 1,
MaxArgs: 1,
Code: shellVolumeName,
NeedsMount: true,
Context: sccNone,
Text: []string{
"setvolume <volume name>",
"",
"Set ProDOS volume name. Truncated to 15 chars if too long.",
},
},
"unmount": &shellCommand{
Name: "unmount",
Description: "unmount disk image",
MinArgs: 0,
MaxArgs: 1,
Code: shellUnmount,
NeedsMount: true,
Context: sccLocal,
Text: []string{
"unmount <slot>",
"",
"Unmount the disk in the specified slot (or current slot)",
},
},
"extract": &shellCommand{
Name: "extract",
Description: "extract file from disk image",
MinArgs: 1,
MaxArgs: -1,
Code: shellExtract,
NeedsMount: true,
Context: sccDiskFile,
Text: []string{
"extract <filename|pattern>",
"",
"Extracts files from current disk",
},
},
"help": &shellCommand{
Name: "help",
Description: "Shows this help",
MinArgs: 0,
MaxArgs: 1,
Code: shellHelp,
NeedsMount: false,
Context: sccCommand,
Text: []string{
"help <command>",
"",
"Display specific help for command or list of commands",
},
},
"info": &shellCommand{
Name: "info",
Description: "Information about the current disk",
MinArgs: -1,
MaxArgs: -1,
Code: shellInfo,
NeedsMount: true,
Context: sccNone,
Text: []string{
"info",
"",
"Display information on current disk",
},
},
"analyze": &shellCommand{
Name: "analyze",
Description: "Process disk using diskm8 analytics",
MinArgs: -1,
MaxArgs: -1,
Code: shellAnalyze,
NeedsMount: true,
Context: sccNone,
Text: []string{
"analyze",
"",
"Display detailed diskm8 information on current disk",
},
},
"quit": &shellCommand{
Name: "quit",
Description: "Leave this place",
MinArgs: -1,
MaxArgs: -1,
Code: shellQuit,
NeedsMount: false,
Context: sccNone,
},
"prefix": &shellCommand{
Name: "prefix",
Description: "Change volume path",
MinArgs: 0,
MaxArgs: 1,
Code: shellPath,
NeedsMount: true,
Context: sccDiskFile,
Text: []string{
"prefix [<path>]",
"",
"Change disk working directory.",
},
},
"cat": &shellCommand{
Name: "cat",
Description: "Display file information",
MinArgs: 0,
MaxArgs: 1,
Code: shellCat,
NeedsMount: true,
Context: sccNone,
Text: []string{
"cat [<pattern>]",
"",
"List files on current disk (can use wildcards).",
},
},
"mkdir": &shellCommand{
Name: "mkdir",
Description: "Create a directory on disk",
MinArgs: 1,
MaxArgs: 1,
Code: shellMkdir,
NeedsMount: true,
Context: sccDiskFile,
Text: []string{
"mkdir <directory>",
"",
"Create directory on current disk (if supported)",
},
},
"put": &shellCommand{
Name: "put",
Description: "Copy local file to disk (with optional target dir)",
MinArgs: 1,
MaxArgs: 2,
Code: shellPut,
NeedsMount: true,
Context: sccLocal,
Text: []string{
"put <local file> [<target dir>]",
"",
"Write local file to current disk",
},
},
"delete": &shellCommand{
Name: "delete",
Description: "Remove file from disk",
MinArgs: 1,
MaxArgs: 1,
Code: shellDelete,
NeedsMount: true,
Context: sccDiskFile,
Text: []string{
"delete <filename>",
"",
"Delete file from current disk",
},
},
"ingest": &shellCommand{
Name: "ingest",
Description: "Ingest directory containing disks (or single disk) into system",
MinArgs: 1,
MaxArgs: 1,
Code: shellIngest,
NeedsMount: false,
Context: sccLocal,
Text: []string{
"ingest <disk name>",
"",
"Catalog diskfile into diskm8 database.",
},
},
"lock": &shellCommand{
Name: "lock",
Description: "Lock file on the disk",
MinArgs: 1,
MaxArgs: 1,
Code: shellLock,
NeedsMount: true,
Context: sccDiskFile,
Text: []string{
"lock <diskfile>",
"",
"Make file on disk read-only",
},
},
"unlock": &shellCommand{
Name: "unlock",
Description: "Unlock file on the disk",
MinArgs: 1,
MaxArgs: 1,
Code: shellUnlock,
NeedsMount: true,
Context: sccDiskFile,
Text: []string{
"unlock <diskfile>",
"",
"Make file on disk writable",
},
},
"ls": &shellCommand{
Name: "ls",
Description: "List local files",
MinArgs: 0,
MaxArgs: 999,
Code: shellListFiles,
NeedsMount: false,
Context: sccLocal,
Text: []string{
"ls <pattern>",
"",
"List local files",
},
},
"cd": &shellCommand{
Name: "cd",
Description: "Change local path",
MinArgs: 0,
MaxArgs: 1,
Code: shellCd,
NeedsMount: false,
Context: sccLocal,
Text: []string{
"cd <path>",
"",
"Change local directory",
},
},
"disks": &shellCommand{
Name: "disks",
Description: "List mounted volumes",
MinArgs: 0,
MaxArgs: 0,
Code: shellDisks,
NeedsMount: false,
Context: sccNone,
Text: []string{
"disks",
"",
"List all mounted volumes",
},
},
"target": &shellCommand{
Name: "target",
Description: "Select mounted volume as default",
MinArgs: 1,
MaxArgs: 1,
Code: shellPrefix,
NeedsMount: false,
Context: sccNone,
Text: []string{
"target <slot>",
"",
"Select slot as default for commands",
},
},
"copy": &shellCommand{
Name: "copy",
Description: "Copy files from one volume to another",
MinArgs: 2,
MaxArgs: 999,
Code: shellD2DCopy,
NeedsMount: false,
Context: sccDiskFile,
Text: []string{
"copy [<slot>:]<pattern> <slot>:[<path>]",
"",
"Copy files from one mounted disk to another.",
"Example:",
"copy 0:*.system 1:",
},
},
"move": &shellCommand{
Name: "move",
Description: "Move files from one volume to another",
MinArgs: 2,
MaxArgs: 999,
Code: shellD2DCopy,
NeedsMount: false,
Context: sccDiskFile,
Text: []string{
"move [<slot>:]<pattern> <slot>:[<path>]",
"",
"Move files from one mounted disk to another.",
"Example:",
"move 0:*.system 1:",
},
},
"rename": &shellCommand{
Name: "rename",
Description: "Rename a file on the disk",
MinArgs: 2,
MaxArgs: 2,
Code: shellRename,
NeedsMount: true,
Context: sccDiskFile,
Text: []string{
"rename <filename> <new filename>",
"",
"Rename a file on a disk.",
},
},
"report": &shellCommand{
Name: "report",
Description: "Run a report",
MinArgs: 1,
MaxArgs: 999,
Code: shellReport,
NeedsMount: false,
Context: sccDiskFile,
Text: []string{
"report <name> [<path>]",
"",
"Reports:",
"as-dupes Active sector dupes report (-as-dupes at command line)",
"file-dupes File dupes report (-file-dupes at command line)",
"whole-dupes Whole disk dupes report (-whole-dupes at command line)",
},
},
"search": &shellCommand{
Name: "search",
Description: "Run a search",
MinArgs: 1,
MaxArgs: 999,
Code: shellSearch,
NeedsMount: false,
Context: sccDiskFile,
Text: []string{
"search <type> [<path>]",
"",
"Searches:",
"filename Search by filename",
"text Search for files containing tex",
"hash Search for files with hash",
},
},
"quarantine": &shellCommand{
Name: "quarantine",
Description: "Like report, but allow moving dupes to a backup folder",
MinArgs: 1,
MaxArgs: 999,
Code: shellQuarantine,
NeedsMount: false,
Context: sccDiskFile,
Text: []string{
"quarantine <name> [<path>]",
"",
"Scans:",
"as-dupes Active sector dupes report (-as-dupes at command line)",
"file-dupes File dupes report (-file-dupes at command line)",
"whole-dupes Whole disk dupes report (-whole-dupes at command line)",
},
},
}
}
func shellProcess(line string) int {
line = strings.TrimSpace(line)
verb, args := smartSplit(line)
if verb != "" {
verb = strings.ToLower(verb)
command, ok := commandList[verb]
if ok {
fmt.Println()
var cok = true
if command.MinArgs != -1 {
if len(args) < command.MinArgs {
os.Stderr.WriteString(fmt.Sprintf("%s expects at least %d arguments\n", verb, command.MinArgs))
cok = false
}
}
if command.MaxArgs != -1 {
if len(args) > command.MaxArgs {
os.Stderr.WriteString(fmt.Sprintf("%s expects at most %d arguments\n", verb, command.MaxArgs))
cok = false
}
}
if command.NeedsMount {
if commandTarget == -1 || commandVolumes[commandTarget] == nil {
os.Stderr.WriteString(fmt.Sprintf("%s only works on mounted disks\n", verb))
cok = false
}
}
if cok {
r := command.Code(args)
fmt.Println()
return r
} else {
return -1
}
} else {
os.Stderr.WriteString(fmt.Sprintf("Unrecognized command: %s\n", verb))
return -1
}
}
return 0
}
func shellDo(dsk *disk.DSKWrapper) {
//commandVolumes = dsk
//commandPath[commandTarget] := ""
ac := &shellCompleter{}
rl, err := readline.NewEx(&readline.Config{
Prompt: getPrompt(commandPath, commandTarget),
HistoryFile: binpath() + "/.shell_history",
DisableAutoSaveHistory: false,
AutoComplete: ac,
})
if err != nil {
//fmt.Println("Error rl:", err)
os.Exit(2)
}
defer rl.Close()
running := true
for running {
line, err := rl.Readline()
if err != nil {
//fmt.Println("Error:", err)
break
}
r := shellProcess(line)
if r == 999 {
//fmt.Println("exit 999")
return
}
rl.SetPrompt(getPrompt(commandPath, commandTarget))
}
}
func shellPath(args []string) int {
path := ""
if len(args) > 0 {
path = args[0]
}
if formatIn(commandVolumes[commandTarget].Format.ID, []disk.DiskFormatID{disk.DF_PRODOS, disk.DF_PRODOS_800KB, disk.DF_PRODOS_400KB, disk.DF_PRODOS_CUSTOM}) {
_, _, _, e := commandVolumes[commandTarget].PRODOSFindDirBlocks(2, path)
if e == nil {
commandPath[commandTarget] = path
if path == "" {
path = "/"
}
fmt.Printf("Switched to directory %s\r\n", path)
} else {
fmt.Println("No such directory")
}
} else {
fmt.Println("Not supported on this filesystem")
}
return 0
}
func shellMount(args []string) int {
if len(args) != 1 {
fmt.Println("mount expects a diskfile")
return -1
}
dsk, err := disk.NewDSKWrapper(defNibbler, args[0])
if err != nil {
os.Stderr.WriteString("Error:" + err.Error() + "\n")
return -1
}
slotid, err := mountDsk(dsk)
if err != nil {
os.Stderr.WriteString("Error:" + err.Error() + "\n")
return -1
}
commandTarget = slotid
os.Stderr.WriteString(fmt.Sprintf("mount disk in slot %d\n", slotid))
return 0
}
func shellUnmount(args []string) int {
if len(args) > 0 {
if shellPrefix(args) == -1 {
return -1
}
}
if commandVolumes[commandTarget] != nil {
commandVolumes[commandTarget] = nil
commandPath[commandTarget] = ""
os.Stderr.WriteString("Unmounted volume\n")
}
return 0
}
func shellHelp(args []string) int {
if len(args) == 0 {
keys := make([]string, 0)
for k, _ := range commandList {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
info := commandList[k]
fmt.Printf("%-10s %s\n", info.Name, info.Description)
}
} else {
command := strings.ToLower(args[0])
if details, ok := commandList[command]; ok {
if details.Text != nil {
for _, l := range details.Text {
fmt.Println(l)
}
} else {
os.Stderr.WriteString("No help available for " + command)
}
} else {
os.Stderr.WriteString("No help available for " + command)
}
}
return 0
}
func shellInfo(args []string) int {
fullpath, _ := filepath.Abs(commandVolumes[commandTarget].Filename)
fmt.Printf("Disk path : %s\n", fullpath)
fmt.Printf("Disk type : %s\n", commandVolumes[commandTarget].Format.String())
fmt.Printf("Sector Order: %s\n", commandVolumes[commandTarget].Layout.String())
fmt.Printf("Size : %d bytes\n", len(commandVolumes[commandTarget].Data))
return 0
}
func shellQuit(args []string) int {
return 999
}
func shellCat(args []string) int {
fullpath, _ := filepath.Abs(commandVolumes[commandTarget].Filename)
info, err := analyze(0, fullpath)
if err != nil {
return -1
}
bs := 256
volumename := "no-name"
if info.FormatID.ID == disk.DF_PASCAL || info.FormatID.ID == disk.DF_PRODOS ||
info.FormatID.ID == disk.DF_PRODOS_800KB || info.FormatID.ID == disk.DF_PRODOS_400KB ||
info.FormatID.ID == disk.DF_PRODOS_CUSTOM {
bs = 512
vdh, err := commandVolumes[commandTarget].PRODOSGetVDH(2)
if err == nil {
volumename = vdh.GetVolumeName()
}
}
pattern := "*"
if len(args) > 0 {
pattern = args[0]
}
files, _ := globDisk(commandTarget, pattern)
fmt.Printf("Volume Name is %s\n\n", volumename)
fmt.Printf("%-33s %6s %2s %-23s %s\n", "NAME", "BLOCKS", "RO", "KIND", "ADDITONAL")
for _, f := range files {
add := ""
locked := " "
if f.LoadAddress != 0 {
add = fmt.Sprintf("(A$%.4X)", f.LoadAddress)
}
if f.Locked {
locked = "Y"
}
fmt.Printf("%-33s %6d %2s %-23s %s\n", f.Filename, (f.Size/bs)+1, locked, f.Type, add)
}
free := 0
used := 0
for _, v := range info.Bitmap {
if v {
used++
} else {
free++
}
}
fmt.Printf("\nUSED: %-20d FREE: %-20d\n", used, free)
return 0
}
func shellCd(args []string) int {
if len(args) > 0 {
err := os.Chdir(args[0])
if err != nil {
os.Stderr.WriteString("Change directory failed: " + err.Error() + "\n")
return -1
}
}
wd, _ := os.Getwd()
os.Stderr.WriteString("Working directory is now " + wd + "\n")
return 0
}
func shellListFiles(args []string) int {
bs := 256
if len(args) == 0 {
wd, _ := os.Getwd()
args = append(args, wd+"/*.*")
}
for _, a := range args {
files, err := filepath.Glob(a)
if err != nil {
os.Stderr.WriteString("Error reading path " + a + ": " + err.Error() + "\n")
continue
}
fmt.Printf("%6s %2s %-23s %s\n", "BLOCKS", "RO", "KIND", "NAME")
for _, f := range files {
locked := " "
fi, _ := os.Stat(f)
if fi.Mode().Perm()&0100 != 0100 {
locked = "Y"
}
fmt.Printf("%6d %2s %-23s %s\n", (int(fi.Size())/bs)+1, locked, "Local file", fi.Name())
}
}
return 0
}
func shellAnalyze(args []string) int {
fullpath, _ := filepath.Abs(commandVolumes[commandTarget].Filename)
info, err := analyze(0, fullpath)
if err != nil {
return -1
}
fmt.Printf("Format: %s\n", info.FormatID)
fmt.Printf("Tracks: %d, Sectors: %d\n", info.Tracks, info.Sectors)
return 0
}
func shellExtract(args []string) int {
fullpath, _ := filepath.Abs(commandVolumes[commandTarget].Filename)
_, err := analyze(0, fullpath)
if err != nil {
return 1
}
fmt.Println("Extract:", args[0])
files, _ := globDisk(commandTarget, args[0])
for _, f := range files {
err := ExtractFile(fullpath, f, true, true)
if err == nil {
fmt.Println("OK")
} else {
fmt.Println("FAILED")
return -1
}
}
return 0
}
func formatIn(f disk.DiskFormatID, list []disk.DiskFormatID) bool {
for _, v := range list {
if v == f {
return true
}
}
return false
}
func fts() string {
t := time.Now()
return fmt.Sprintf(
"%.4d%.2d%.2d%.2d%.2d%.2d",
t.Year(), t.Month(), t.Day(),
t.Hour(), t.Minute(), t.Second(),
)
}
func backupFile(path string) error {
data, err := ioutil.ReadFile(path)
if err != nil {
return err
}
path = strings.Replace(path, ":", "", -1)
path = strings.Replace(path, "\\", "/", -1)
bpath := binpath() + "/backup/" + path + "." + fts()
os.MkdirAll(filepath.Dir(bpath), 0755)
f, err := os.Create(bpath)
if err != nil {
return err
}
f.Write(data)
f.Close()
os.Stderr.WriteString("Backed up disk to: " + bpath + "\n")
return nil
}
func saveDisk(dsk *disk.DSKWrapper, path string) error {
backupFile(path)
f, e := os.Create(path)
if e != nil {
return e
}
defer f.Close()
f.Write(dsk.Data)
fmt.Println("Updated disk " + path)
return nil
}
func shellMkdir(args []string) int {
fullpath, _ := filepath.Abs(commandVolumes[commandTarget].Filename)
_, err := analyze(0, fullpath)
if err != nil {
return 1
}
path := ""
name := args[0]
if strings.Contains(name, "/") {
path = filepath.Dir(name)
name = filepath.Base(name)
}
if formatIn(commandVolumes[commandTarget].Format.ID, []disk.DiskFormatID{disk.DF_PRODOS, disk.DF_PRODOS_800KB, disk.DF_PRODOS_400KB, disk.DF_PRODOS_CUSTOM}) {
e := commandVolumes[commandTarget].PRODOSCreateDirectory(path, name)
if e != nil {
fmt.Println(e)
return -1
}
saveDisk(commandVolumes[commandTarget], fullpath)
} else {
fmt.Println("Do not support Mkdir on " + commandVolumes[commandTarget].Format.String() + " currently.")
return 0
}
return 0
}
func shellVolumeName(args []string) int {
fullpath, _ := filepath.Abs(commandVolumes[commandTarget].Filename)
_, err := analyze(0, fullpath)
if err != nil {
return 1
}
name := strings.ToUpper(args[0])
if formatIn(commandVolumes[commandTarget].Format.ID, []disk.DiskFormatID{disk.DF_PRODOS, disk.DF_PRODOS_800KB, disk.DF_PRODOS_400KB, disk.DF_PRODOS_CUSTOM}) {
vdh, err := commandVolumes[commandTarget].PRODOSGetVDH(2)
if err != nil {
fmt.Printf("Failed to get Volume Directory Header: %v\n", err)
return -1
}
vdh.SetVolumeName(name)
commandVolumes[commandTarget].PRODOSSetVDH(2, vdh)
fmt.Printf("Volume name is now %s\n", vdh.GetVolumeName())
saveDisk(commandVolumes[commandTarget], fullpath)
} else {
fmt.Println("Do not support setvolume on " + commandVolumes[commandTarget].Format.String() + ".")
return 0
}
return 0
}
func isASCII(in []byte) bool {
for _, v := range in {
if v > 128 {
return false
}
}
return true
}
func shellPut(args []string) int {
fullpath, _ := filepath.Abs(commandVolumes[commandTarget].Filename)
_, err := analyze(0, fullpath)
if err != nil {
return 1
}
parts := strings.Split(args[0], ",")
data, err := ioutil.ReadFile(parts[0])
if err != nil {
return -1
}
addr := int64(0x0801)
name := filepath.Base(args[0])
reTrailAddr := regexp.MustCompile("(?i)^([^,]+)([,]A(([$]|0x)[0-9a-f]+))?([,]L(([$]|0x)[0-9a-f]+))?$")
if reTrailAddr.MatchString(name) {
m := reTrailAddr.FindAllStringSubmatch(name, -1)
name = m[0][1]
saddr := m[0][3]
slen := m[0][6]
if saddr != "" {
if strings.HasPrefix(saddr, "$") {
saddr = "0x" + saddr[1:]
}
addr, _ = strconv.ParseInt(saddr, 0, 32)
}
if slen != "" {
if strings.HasPrefix(slen, "$") {
slen = "0x" + slen[1:]
}
nlen, _ := strconv.ParseInt(slen, 0, 32)
if int(nlen) < len(data) {
data = data[:int(nlen)]
}
}
}
if formatIn(commandVolumes[commandTarget].Format.ID, []disk.DiskFormatID{disk.DF_DOS_SECTORS_13, disk.DF_DOS_SECTORS_16}) {
kind := disk.FileTypeAPP
reSpecial := regexp.MustCompile("(?i)^(.+)[#](0x[a-fA-F0-9]+)[.]([A-Za-z]+)$")
ext := strings.Trim(filepath.Ext(name), ".")
if reSpecial.MatchString(name) {
m := reSpecial.FindAllStringSubmatch(name, -1)
name = m[0][1]
ext = strings.ToLower(m[0][3])
addrStr := m[0][2]
addr, _ = strconv.ParseInt(addrStr, 0, 32)
} else {
//name = strings.Replace(name, "."+ext, "", -1)
l := len(ext) + 1
name = name[:len(name)-l]
}
kind = disk.AppleDOSFileTypeFromExt(ext)
if strings.HasSuffix(args[0], ".INT.ASC") {
kind = disk.FileTypeINT
} else if strings.HasSuffix(args[0], ".APP.ASC") {
kind = disk.FileTypeAPP
}
if kind == disk.FileTypeAPP && isASCII(data) {
lines := strings.Split(string(data), "\n")
data = disk.ApplesoftTokenize(lines)
} else if kind == disk.FileTypeINT && isASCII(data) {
lines := strings.Split(string(data), "\n")
data = disk.IntegerTokenize(lines)
os.Stderr.WriteString("WARNING: Integer retokenization from text is experimental\n")
}
e := commandVolumes[commandTarget].AppleDOSWriteFile(name, kind, data, int(addr))
if e != nil {
os.Stderr.WriteString("Failed to create file: " + e.Error())
return -1
}
saveDisk(commandVolumes[commandTarget], fullpath)
} else if formatIn(commandVolumes[commandTarget].Format.ID, []disk.DiskFormatID{disk.DF_PRODOS, disk.DF_PRODOS_800KB, disk.DF_PRODOS_400KB, disk.DF_PRODOS_CUSTOM}) {
ext := strings.Trim(filepath.Ext(name), ".")
reSpecial := regexp.MustCompile("(?i)^(.+)[#](0x[a-fA-F0-9]+)[.]([A-Za-z]+)$")
if reSpecial.MatchString(name) {
m := reSpecial.FindAllStringSubmatch(name, -1)
name = m[0][1]
ext = strings.ToLower(m[0][3])
addrStr := m[0][2]
addr, _ = strconv.ParseInt(addrStr, 0, 32)
} else {
l := len(ext) + 1
name = name[:len(name)-l]
}
kind := disk.ProDOSFileTypeFromExt(ext)
if strings.ToLower(ext) == "system" {
name += "." + ext
ext = ""
kind = disk.ProDOSFileTypeFromExt("SYS")
}
if strings.HasSuffix(args[0], ".INT.ASC") {
kind = disk.FileType_PD_INT
} else if strings.HasSuffix(args[0], ".APP.ASC") {
kind = disk.FileType_PD_APP
}
if kind == disk.FileType_PD_APP && isASCII(data) {
lines := strings.Split(string(data), "\n")
data = disk.ApplesoftTokenize(lines)
} else if kind == disk.FileType_PD_INT && isASCII(data) {
lines := strings.Split(string(data), "\n")
data = disk.IntegerTokenize(lines)
os.Stderr.WriteString("WARNING: Integer retokenization from text is experimental\n")
}
e := commandVolumes[commandTarget].PRODOSWriteFile(commandPath[commandTarget], name, kind, data, int(addr))
if e != nil {
os.Stderr.WriteString("Failed to create file: " + e.Error())
return -1
}
saveDisk(commandVolumes[commandTarget], fullpath)
} else {
os.Stderr.WriteString("Writing files not supported on " + commandVolumes[commandTarget].Format.String())
return -1
}
return 0
}
func shellDelete(args []string) int {
fullpath, _ := filepath.Abs(commandVolumes[commandTarget].Filename)
_, err := analyze(0, fullpath)
if err != nil {
return 1
}
if formatIn(commandVolumes[commandTarget].Format.ID, []disk.DiskFormatID{disk.DF_DOS_SECTORS_13, disk.DF_DOS_SECTORS_16}) {
err = commandVolumes[commandTarget].AppleDOSDeleteFile(args[0])
if err != nil {
os.Stderr.WriteString(err.Error())
return -1
}
saveDisk(commandVolumes[commandTarget], fullpath)
} else if formatIn(commandVolumes[commandTarget].Format.ID, []disk.DiskFormatID{disk.DF_PRODOS, disk.DF_PRODOS_800KB, disk.DF_PRODOS_400KB, disk.DF_PRODOS_CUSTOM}) {
path := commandPath[commandTarget]
if strings.Contains(args[0], "/") {
path = filepath.Dir(args[0])
args[0] = filepath.Base(args[0])
}
err = commandVolumes[commandTarget].PRODOSDeleteFile(path, args[0])
if err != nil {
os.Stderr.WriteString(err.Error())
return -1
}
saveDisk(commandVolumes[commandTarget], fullpath)
} else {
os.Stderr.WriteString("Deleting files not supported on " + commandVolumes[commandTarget].Format.String())
return -1
}
return 0
}
func shellIngest(args []string) int {
processed = 0
dskName := args[0]
info, err := os.Stat(dskName)
if err != nil {
loggy.Get(0).Errorf("Error stating file: %s", err.Error())
os.Exit(2)
}
if info.IsDir() {
walk(dskName)
} else {
indisk = make(map[disk.DiskFormat]int)
outdisk = make(map[disk.DiskFormat]int)
panic.Do(
func() {
var e error
_, e = analyze(0, dskName)
// handle any disk specific
if e != nil {
os.Stderr.WriteString("Error processing disk")
}
},
func(r interface{}) {
loggy.Get(0).Errorf("Error processing volume: %s", dskName)
loggy.Get(0).Errorf(string(debug.Stack()))
},
)
}
return 0
}
func shellLock(args []string) int {
fullpath, _ := filepath.Abs(commandVolumes[commandTarget].Filename)
_, err := analyze(0, fullpath)
if err != nil {
return 1
}
if formatIn(commandVolumes[commandTarget].Format.ID, []disk.DiskFormatID{disk.DF_DOS_SECTORS_13, disk.DF_DOS_SECTORS_16}) {
err = commandVolumes[commandTarget].AppleDOSSetLocked(args[0], true)
if err != nil {
os.Stderr.WriteString(err.Error())
return -1
}
saveDisk(commandVolumes[commandTarget], fullpath)
} else if formatIn(commandVolumes[commandTarget].Format.ID, []disk.DiskFormatID{disk.DF_PRODOS, disk.DF_PRODOS_800KB, disk.DF_PRODOS_400KB, disk.DF_PRODOS_CUSTOM}) {
path := commandPath[commandTarget]
if strings.Contains(args[0], "/") {
path = filepath.Dir(args[0])
args[0] = filepath.Base(args[0])
}
err = commandVolumes[commandTarget].PRODOSSetLocked(path, args[0], true)
if err != nil {
os.Stderr.WriteString(err.Error())
return -1
}
saveDisk(commandVolumes[commandTarget], fullpath)
} else {
os.Stderr.WriteString("Locking files not supported on " + commandVolumes[commandTarget].Format.String())
return -1
}
return 0
}
func shellUnlock(args []string) int {
fullpath, _ := filepath.Abs(commandVolumes[commandTarget].Filename)
_, err := analyze(0, fullpath)
if err != nil {
return 1
}
if formatIn(commandVolumes[commandTarget].Format.ID, []disk.DiskFormatID{disk.DF_DOS_SECTORS_13, disk.DF_DOS_SECTORS_16}) {
err = commandVolumes[commandTarget].AppleDOSSetLocked(args[0], false)
if err != nil {
os.Stderr.WriteString(err.Error())
return -1
}
saveDisk(commandVolumes[commandTarget], fullpath)
} else if formatIn(commandVolumes[commandTarget].Format.ID, []disk.DiskFormatID{disk.DF_PRODOS, disk.DF_PRODOS_800KB, disk.DF_PRODOS_400KB, disk.DF_PRODOS_CUSTOM}) {
path := commandPath[commandTarget]
if strings.Contains(args[0], "/") {
path = filepath.Dir(args[0])
args[0] = filepath.Base(args[0])
}
err = commandVolumes[commandTarget].PRODOSSetLocked(path, args[0], false)
if err != nil {
os.Stderr.WriteString(err.Error())
return -1
}
saveDisk(commandVolumes[commandTarget], fullpath)
} else {
os.Stderr.WriteString("Locking files not supported on " + commandVolumes[commandTarget].Format.String())
return -1
}
return 0
}
func shellDisks(args []string) int {
fmt.Println("Mounted Volumes")
for i, d := range commandVolumes {
if d != nil {
fmt.Printf("%d:%s\n", i, d.Filename)
}
}
return 0
}
func shellPrefix(args []string) int {
tmp, err := strconv.ParseInt(args[0], 10, 32)
if err != nil {
os.Stderr.WriteString("Invalid slot number: " + args[0] + "\n")
return -1
}
slotid := int(tmp)
if slotid < 0 || slotid >= MAXVOL {
os.Stderr.WriteString(fmt.Sprintf("Valid slots are %d to %d.\n", 0, MAXVOL-1))
return -1
}
d := commandVolumes[slotid]
if d == nil {
os.Stderr.WriteString(fmt.Sprintf("Nothing mounted in slot %d (use disks to see mounts)\n", slotid))
return -1
}
commandTarget = slotid
return 0
}
func shellD2DCopy(args []string) int {
reCopyArg := regexp.MustCompile("(?i)^(([0-9])[:])?(.+)$")
reCopyTarget := regexp.MustCompile("(?i)^(([0-9])[:])?(.+)?$")
l := len(args)
sources := args[0 : l-1]
target := args[l-1]
var allfiles []*DiskFile
for _, arg := range sources {
if reCopyArg.MatchString(arg) {
m := reCopyArg.FindAllStringSubmatch(arg, -1)
volume := commandTarget
if m[0][2] != "" {
tmp, err := strconv.ParseInt(m[0][2], 10, 32)
if err != nil {
os.Stderr.WriteString("Invalid slot number: " + m[0][2] + "\n")
return -1
}
volume = int(tmp)
}
patternstr := m[0][3]
files, _ := globDisk(volume, patternstr)
allfiles = append(allfiles, files...)
}
}
if reCopyTarget.MatchString(target) {
m := reCopyTarget.FindAllStringSubmatch(target, -1)
volume := commandTarget
if m[0][2] != "" {
tmp, err := strconv.ParseInt(m[0][2], 10, 32)
if err != nil {
os.Stderr.WriteString("Invalid slot number: " + m[0][2] + "\n")
return -1
}
volume = int(tmp)
}
path := m[0][3]
v := commandVolumes[volume]
if v == nil {
os.Stderr.WriteString("Invalid slot number: " + m[0][2] + "\n")
return -1
}
if !formatIn(
v.Format.ID,
[]disk.DiskFormatID{
disk.DF_DOS_SECTORS_13,
disk.DF_DOS_SECTORS_16,
disk.DF_PRODOS,
disk.DF_PRODOS_800KB,
disk.DF_PRODOS_400KB,
disk.DF_PRODOS_CUSTOM,
}) {
os.Stderr.WriteString("Target volume does not support write.\n")
return -1
}
if path != "" && len(allfiles) > 1 {
// copy to path
if !formatIn(
v.Format.ID,
[]disk.DiskFormatID{
disk.DF_PRODOS,
disk.DF_PRODOS_800KB,
disk.DF_PRODOS_400KB,
disk.DF_PRODOS_CUSTOM,
}) {
os.Stderr.WriteString("Only prodos supports copy to directory")
return -1
}
for _, f := range allfiles {
// must be prodos
name := f.Filename
if len(name) > 15 {
name = name[:15]
}
kind := disk.ProDOSFileTypeFromExt(f.Ext)
auxtype := f.LoadAddress
data := f.Data
e := v.PRODOSWriteFile(path, name, kind, data, auxtype)
if e != nil {
os.Stderr.WriteString(fmt.Sprintf("Failed to copy %s: %s\n", name, e.Error()))
return -1
}
os.Stderr.WriteString(fmt.Sprintf("Copied %s (%d bytes)\n", name, len(data)))
}
} else {
for _, f := range allfiles {
name := f.Filename
if path != "" && len(allfiles) == 1 {
name = path
}
if formatIn(
v.Format.ID,
[]disk.DiskFormatID{
disk.DF_PRODOS,
disk.DF_PRODOS_800KB,
disk.DF_PRODOS_400KB,
disk.DF_PRODOS_CUSTOM,
}) {
if len(name) > 15 {
name = name[:15]
}
kind := disk.ProDOSFileTypeFromExt(f.Ext)
auxtype := f.LoadAddress
data := f.Data
e := v.PRODOSWriteFile("", name, kind, data, auxtype)
if e != nil {
os.Stderr.WriteString(fmt.Sprintf("Failed to copy %s: %s\n", name, e.Error()))
return -1
}
os.Stderr.WriteString(fmt.Sprintf("Copied %s (%d bytes)\n", name, len(data)))
} else {
// DOS
kind := disk.AppleDOSFileTypeFromExt(f.Ext)
auxtype := f.LoadAddress
data := f.Data
e := v.AppleDOSWriteFile(name, kind, data, auxtype)
if e != nil {
os.Stderr.WriteString(fmt.Sprintf("Failed to copy %s: %s\n", name, e.Error()))
return -1
}
os.Stderr.WriteString(fmt.Sprintf("Copied %s (%d bytes)\n", name, len(data)))
}
}
}
// here need to publish disk
fullpath, _ := filepath.Abs(v.Filename)
saveDisk(v, fullpath)
} else {
os.Stderr.WriteString("Invalid target: " + target + "\n")
return -1
}
return 0
}
func shellRename(args []string) int {
fullpath, _ := filepath.Abs(commandVolumes[commandTarget].Filename)
if formatIn(commandVolumes[commandTarget].Format.ID, []disk.DiskFormatID{disk.DF_PRODOS, disk.DF_PRODOS_800KB, disk.DF_PRODOS_400KB, disk.DF_PRODOS_CUSTOM}) {
oldname := filepath.Base(args[0])
oldpath := filepath.Dir(args[0])
newname := filepath.Base(args[1])
if oldpath == "." {
oldpath = ""
}
fmt.Println(oldname, newname, oldpath)
e := commandVolumes[commandTarget].PRODOSRenameFile(oldpath, oldname, newname)
if e != nil {
os.Stderr.WriteString("Unable to rename file: " + e.Error())
return -1
}
} else if formatIn(commandVolumes[commandTarget].Format.ID, []disk.DiskFormatID{disk.DF_DOS_SECTORS_13, disk.DF_DOS_SECTORS_16}) {
oldname := filepath.Base(args[0])
newname := filepath.Base(args[1])
e := commandVolumes[commandTarget].AppleDOSRenameFile(oldname, newname)
if e != nil {
os.Stderr.WriteString("Unable to rename file: " + e.Error())
return -1
}
} else {
os.Stderr.WriteString("Rename currently unsupported on " + commandVolumes[commandTarget].Format.String() + "\n")
return -1
}
saveDisk(commandVolumes[commandTarget], fullpath)
return 0
}
func globDisk(slotid int, pattern string) ([]*DiskFile, error) {
var files []*DiskFile
if commandVolumes[slotid] == nil {
return []*DiskFile(nil), fmt.Errorf("Invalid slotid %d", slotid)
}
fullpath, _ := filepath.Abs(commandVolumes[slotid].Filename)
dsk, err := analyze(0, fullpath)
if err != nil {
return []*DiskFile(nil), fmt.Errorf("Problem reading volume")
}
r := strings.Replace(pattern, ".", "[.]", -1)
r = strings.Replace(r, "?", ".", -1)
r = strings.Replace(r, "*", ".*", -1)
r = "(?i)^" + r + "$"
rePattern := regexp.MustCompile(r)
for _, f := range dsk.Files {
if rePattern.MatchString(f.Filename) {
files = append(files, f)
}
}
return files, nil
}
func shellReport(args []string) int {
switch args[0] {
case "as-dupes":
activeDupeReport(args[1:])
case "file-dupes":
fileDupeReport(args[1:])
case "whole-dupes":
wholeDupeReport(args[1:])
}
return -1
}
func shellSearch(args []string) int {
switch args[0] {
case "text":
//activeDupeReport(args[1:])
searchForTEXT(args[1], args[2:])
case "filename":
//fileDupeReport(args[1:])
searchForFilename(args[1], args[2:])
case "hash":
searchForSHA256(args[1], args[2:])
}
return -1
}
func shellQuarantine(args []string) int {
switch args[0] {
case "as-dupes":
quarantineActiveDisks(args[1:])
case "whole-dupes":
quarantineWholeDisks(args[1:])
}
return -1
}
func moveFile(source, dest string) error {
source = strings.Replace(source, "\\", "/", -1)
dest = strings.Replace(dest, "\\", "/", -1)
fmt.Printf("Reading source file: %s\n", source)
data, err := ioutil.ReadFile(source)
if err != nil {
return err
}
// make sure dest dir actually exists
os.MkdirAll(filepath.Dir(dest), 0755)
fmt.Printf("Creating dest file: %s\n", dest)
f, err := os.Create(dest)
if err != nil {
return err
}
f.Write(data)
f.Close()
err = os.Remove(source)
if err != nil {
return err
}
if _, err := os.Stat(source); err == nil {
fmt.Println(source + " not deleted!!")
return errors.New(source + " not deleted!!")
}
return nil
}
func quarantineActiveDisks(filter []string) {
dfc := &DuplicateActiveSectorDiskCollection{}
Aggregate(AggregateDuplicateActiveSectorDisks, dfc, filter)
reader := bufio.NewReader(os.Stdin)
for _, list := range dfc.data {
if len(list) == 1 {
continue
}
prompt:
fmt.Println("Which one to keep?")
fmt.Println("(0) Skip this...")
for i, v := range list {
fmt.Printf("(%d) %s\n", i+1, v.Fullpath)
}
fmt.Println()
fmt.Printf("Option (0-%d, q): ", len(list))
text, _ := reader.ReadString('\n')
text = strings.ToLower(strings.Trim(text, "\r\n"))
if text == "q" {
return
}
if text == "0" {
continue
}
tmp, _ := strconv.ParseInt(text, 10, 32)
idx := int(tmp) - 1
if idx < 0 || idx > len(list) {
goto prompt
}
for i, v := range list {
if i == idx {
continue
}
path := v.Fullpath
path = strings.Replace(path, ":", "", -1)
path = strings.Replace(path, "\\", "/", -1)
bpath := binpath() + "/quarantine/" + path
err := moveFile(v.Fullpath, bpath)
if err != nil {
fmt.Println(err)
return
}
err = moveFile(v.fingerprint, v.fingerprint+".q")
if err != nil {
fmt.Println(err)
return
}
}
}
}
func quarantineWholeDisks(filter []string) {
dfc := &DuplicateWholeDiskCollection{}
Aggregate(AggregateDuplicateWholeDisks, dfc, filter)
reader := bufio.NewReader(os.Stdin)
for _, list := range dfc.data {
if len(list) == 1 {
continue
}
wprompt:
fmt.Println("Which one to keep?")
fmt.Println("(0) Skip this...")
for i, v := range list {
fmt.Printf("(%d) %s\n", i+1, v.Fullpath)
}
fmt.Println()
fmt.Printf("Option (0-%d, q): ", len(list))
text, _ := reader.ReadString('\n')
text = strings.ToLower(strings.Trim(text, "\r\n"))
if text == "q" {
return
}
if text == "0" {
continue
}
tmp, _ := strconv.ParseInt(text, 10, 32)
idx := int(tmp) - 1
if idx < 0 || idx > len(list) {
goto wprompt
}
for i, v := range list {
if i == idx {
continue
}
path := v.Fullpath
path = strings.Replace(path, ":", "", -1)
path = strings.Replace(path, "\\", "/", -1)
bpath := binpath() + "/quarantine/" + path
err := moveFile(v.Fullpath, bpath)
if err != nil {
fmt.Println(err)
return
}
err = moveFile(v.fingerprint, v.fingerprint+".q")
if err != nil {
fmt.Println(err)
return
}
}
}
}