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, "", 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 ", "", "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 ", "", "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 ", "", "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 ", "", "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 ", "", "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 []", "", "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 []", "", "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 ", "", "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 []", "", "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 ", "", "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 ", "", "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 ", "", "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 ", "", "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 ", "", "List local files", }, }, "cd": &shellCommand{ Name: "cd", Description: "Change local path", MinArgs: 0, MaxArgs: 1, Code: shellCd, NeedsMount: false, Context: sccLocal, Text: []string{ "cd ", "", "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 ", "", "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 [:] :[]", "", "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 [:] :[]", "", "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 ", "", "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 []", "", "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 []", "", "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 []", "", "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 } } } }