mirror of
https://github.com/paleotronic/diskm8.git
synced 2024-10-06 12:55:47 +00:00
1864 lines
40 KiB
Go
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
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
}
|