You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
nimf/helpers.go

545 lines
12 KiB

package main
import (
"bufio"
"crypto/tls"
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"net/url"
"os"
"os/user"
"path"
"path/filepath"
"regexp"
"strconv"
"strings"
)
var (
reWordStart *regexp.Regexp = regexp.MustCompile(`(?m)(?:^|\s):(?:$|\s)`)
reWordEnd *regexp.Regexp = regexp.MustCompile(`(?m)(?:^|\s);(?:$|\s)`)
reStringBoundary *regexp.Regexp = regexp.MustCompile(`(?:[^\\]|^)"`)
)
type stack struct {
stack []int
Pointer int
}
func NewStack(depth int) stack {
return stack{stack: make([]int, depth), Pointer: -1}
}
func (s *stack) Pop() int {
if s.Pointer < 0 {
panic("Stack underflow")
}
s.Pointer--
return s.stack[s.Pointer+1]
}
func (s *stack) Push(v int) {
if s.Pointer == stackDepth {
panic("Stack overflow")
}
s.Pointer++
s.stack[s.Pointer] = v
}
func (s *stack) Peep() int {
if s.Pointer < 0 || s.Pointer >= stackDepth {
panic("The stack is empty")
}
return s.stack[s.Pointer]
}
func nameExists(n string) bool {
if _, ok := names[n]; ok {
return true
}
return false
}
func bool2Int(f bool) int {
o := 0
if f {
o = ^o
}
return o
}
func recoverFromStackOverUnder() {
if r := recover(); r != nil {
data.Pointer, address.Pointer = -1, -1
handleError(fmt.Errorf("%v", r))
}
}
func unescapeString(s string) string {
s = strings.Replace(s, "\\0o", "\\0", -1)
var out strings.Builder
escapeNumBase := 10
var altNum bool
var otherNum strings.Builder
var slash bool
for _, c := range []rune(s) {
if slash && !altNum {
switch c {
case 't':
out.WriteRune('\t')
case 'n':
out.WriteRune('\n')
case 'r':
out.WriteRune('\r')
case 'v':
out.WriteRune('\v')
case 'e':
out.WriteRune(27)
case 'a':
out.WriteRune('\a')
case 'b':
out.WriteRune('\b')
case '\\':
out.WriteRune('\\')
case 'f':
out.WriteRune('\f')
case '_':
out.WriteRune(' ')
case '0':
escapeNumBase = 8
altNum = true
continue
case '1', '3', '4', '2', '5', '6', '7', '8', '9':
altNum = true
escapeNumBase = 10
otherNum.WriteRune(c)
continue
default:
out.WriteRune(c)
}
slash = false
} else if slash {
switch c {
case '0', '1', '3', '4', '2', '5', '6', '7', '8', '9':
otherNum.WriteRune(c)
case 'x':
if otherNum.String() == "" {
escapeNumBase = 16
continue
}
fallthrough
case 'A', 'B', 'C', 'D', 'E', 'F':
if escapeNumBase == 16 {
otherNum.WriteRune(c)
continue
}
fallthrough
default:
altNum = false
slash = false
if otherNum.Len() > 0 {
i, err := strconv.ParseInt(otherNum.String(), escapeNumBase, 64)
if err == nil {
out.WriteRune(rune(i))
} else {
out.WriteRune('?')
}
otherNum.Reset()
}
if c == '\\' {
slash = true
} else {
out.WriteRune(c)
}
}
} else if c == '\\' {
slash = true
} else {
out.WriteRune(c)
}
}
if otherNum.Len() > 0 {
i, err := strconv.ParseInt(otherNum.String(), escapeNumBase, 64)
if err == nil {
out.WriteRune(rune(i))
} else {
out.WriteRune('?')
}
}
return out.String()
}
func storeTempString(s string) {
memory.Set(50, len(s))
counter := 1
for _, c := range []rune(s) {
memory.Set(50+counter, int(c))
counter++
}
}
// If in string mode, adds to the tempstring
// and increments the tspointer as needed
// func addToStoredString(text string, pointer *int) {
// rslice := []rune(text)
// if *pointer > 52 && memory.Get(*pointer-1) != '\n' {
// memory.Set(*pointer, int(' '))
// *pointer++
// }
// for _, char := range rslice {
// memory.Set(*pointer, int(char))
// *pointer++
// }
// }
func storeNewString(name string) {
defer memory.Set(svarAssignmentFlag, 0)
_, err := strconv.Atoi(name)
if err == nil {
handleError(fmt.Errorf("Invalid svar name: %q", name))
return
}
if _, ok := names[name]; ok {
handleError(fmt.Errorf("Svar %q already exists", name))
return
}
names[name] = []token{token{t: strconv.Itoa(memory.NewSvarPointer()), line: -1, file: "runtime", tokType: intType}}
}
func nimfStringToGolang(address int) (string, error) {
length := memory.Get(address)
if length <= 0 {
return "", fmt.Errorf("Invalid string")
}
var text strings.Builder
for i := address + 1; i <= address+length; i++ {
text.WriteRune(rune(memory.Get(i)))
}
return text.String(), nil
}
func golangStringToNimf(text string) {
memory.Set(tempString, len(text))
memory.tspointer = 51
for _, c := range text {
if memory.tspointer > 29999 {
panic("Temporary string buffer overflow, string writing has been truncated")
}
memory.Set(memory.tspointer, int(c))
memory.tspointer++
}
}
func expandedAbsFilepath(p string) string {
if strings.HasPrefix(p, "~") {
if p == "~" || strings.HasPrefix(p, "~/") {
homedir, _ := os.UserHomeDir()
if len(p) <= 2 {
p = homedir
} else if len(p) > 2 {
p = filepath.Join(homedir, p[2:])
}
} else {
i := strings.IndexRune(p, '/')
var u string
var remainder string
if i < 0 {
u = p[1:]
remainder = ""
} else {
u = p[1:i]
remainder = p[i:]
}
usr, err := user.Lookup(u)
if err != nil {
p = filepath.Join("/home", u, remainder)
} else {
p = filepath.Join(usr.HomeDir, remainder)
}
}
} else if !strings.HasPrefix(p, "/") {
wd, _ := os.Getwd()
p = filepath.Join(wd, p)
}
path, _ := filepath.Abs(p)
return path
}
func cleanUpEarlyExit(remainingAction []token) {
thenSkip := 0
loopSkip := 0
for _, val := range remainingAction {
switch val.t.(string) {
case "if":
thenSkip += 1
case "do":
loopSkip += 1
case "then":
if thenSkip > 0 {
thenSkip -= 1
} else {
address.Pop()
memory.Set(conditionalDepthCounter, memory.Get(conditionalDepthCounter)-1)
priorRunState := address.Pop()
if memory.ValueEquals(conditionalDepthCounter, 0) {
memory.Set(conditionalBranchRunFlag, -1)
} else {
memory.Set(conditionalBranchRunFlag, priorRunState)
}
}
case "loop":
if loopSkip > 0 {
loopSkip -= 1
} else {
address.Pop()
memory.Set(loopSetFlag, 0) // Set the loop return flag
}
}
}
memory.Set(actionModeFlag, 0) // Turn off action mode
memory.Set(exitFlag, 0) // Turn the exit flag off
}
func cleanUpLocalMemory(start int) {
for i := start; i <= memory.lpointer; i++ {
memory.Set(memory.pointer+i, 0)
}
memory.lpointer = start
}
func readMoreInput(text string) bool {
wordStartCount := len(reWordStart.FindAllStringIndex(text, -1))
wordEndCount := len(reWordEnd.FindAllStringIndex(text, -1))
stringBoundaryCount := len(reStringBoundary.FindAllStringIndex(text, -1))
return (wordStartCount != wordEndCount || stringBoundaryCount%2 != 0)
}
func getReplInput() []token {
reader := bufio.NewReader(os.Stdin)
nl := ""
if memory.Get(outputPrintedFlag) == -1 {
nl = "\n"
memory.Set(outputPrintedFlag, 0)
}
fmt.Printf("%s\033[1;32m> \033[0m", nl)
text, err := reader.ReadString('\n')
if err != nil {
panic("Error reading line at repl")
}
for readMoreInput(text) {
fmt.Printf("%s\033[2;32m> \033[0m", nl)
ntext, err := reader.ReadString('\n')
if err != nil {
panic("Error reading line at repl")
}
text = text + ntext
}
return parse(text, true)
}
func handleParseError(e error, l int, f, tok string) {
errTemplate := "\033[91;1mError: \033[0m\033[1m%s\033[0m\n\nline : %d\nfile : %s\ntoken: %s\n\n"
fmt.Fprintf(os.Stderr, errTemplate, e.Error(), l, f, tok)
if interactive {
parseError = true
reset = true
data.Pointer, address.Pointer = -1, -1
memory.Set(3, 0) // string mode is off
memory.Set(4, 0) // string mode is off
memory.Set(8, 0) // branch depth is 0
memory.Set(9, 0) // loop depth is 0
memory.Set(11, 0) // variable setting is off
} else {
os.Exit(1)
}
}
func handleError(e error) {
line := -1
file := "error tracing file"
token := "error tracing token"
t, err := scanner.PeekLast()
if err != nil {
t, err = scanner.Read()
if err == nil {
scanner.Unread()
line = t.line
file = t.file
token = t.String()
}
} else {
line = t.line
file = t.file
token = t.String()
}
errTemplate := "\033[91;1mError: \033[0m\033[1m%s\033[0m\n\nline : %d\nfile : %s\ntoken: %s\n\n"
fmt.Fprintf(os.Stderr, errTemplate, e.Error(), line, file, token)
if interactive {
reset = true
data.Pointer, address.Pointer = -1, -1
memory.Set(3, 0) // string mode is off
memory.Set(4, 0) // string mode is off
memory.Set(8, 0) // branch depth is 0
memory.Set(9, 0) // loop depth is 0
memory.Set(11, 0) // variable setting is off
} else {
os.Exit(1)
}
}
func writeLib(name string, data []byte) {
if !strings.HasSuffix(name, ".nh") {
name = name + ".nh"
}
xdg_lib_path := os.Getenv("XDG_DATA_HOME")
if xdg_lib_path == "" {
xdg_lib_path = "~/.local/share/nimf/"
} else {
xdg_lib_path = filepath.Join(xdg_lib_path, "nimf")
}
_ = os.MkdirAll(xdg_lib_path, 0644)
modeFlag := os.O_WRONLY | os.O_CREATE | os.O_TRUNC
file, err := os.OpenFile(filepath.Join(xdg_lib_path, name), modeFlag, 0644)
if err != nil {
fmt.Fprintf(os.Stderr, "Could not install file. Write failed. %s\n", err.Error())
os.Exit(1)
}
defer file.Close()
file.Write(data)
}
func getGemini(s string) []byte {
i := 1
conf := &tls.Config{
MinVersion: tls.VersionTLS12,
InsecureSkipVerify: true,
}
var data string
for i < 5 {
u, err := url.Parse(s)
if err != nil {
fmt.Fprintf(os.Stderr, "Could not install file. Invalid URL. %s\n", err.Error())
os.Exit(1)
}
port := u.Port()
if port == "" {
port = "1965"
}
c, err := tls.Dial("tcp", u.Host+":"+port, conf)
if err != nil {
fmt.Fprintf(os.Stderr, "Could not install file. Could not connect to server. %s\n", err.Error())
os.Exit(1)
}
defer c.Close()
send := u.String() + "\r\n"
_, err = c.Write([]byte(send))
if err != nil {
fmt.Fprintf(os.Stderr, "Could not install file. Could not write to server. %s\n", err.Error())
os.Exit(1)
}
result, err := ioutil.ReadAll(c)
if err != nil {
fmt.Fprintf(os.Stderr, "Could not install file. Could not read from server. %s\n", err.Error())
os.Exit(1)
}
data = string(result)
endHeader := strings.Index(data, "\n")
if endHeader < 1 {
fmt.Fprintf(os.Stderr, "Could not install file. Invalid server response. %s\n", err.Error())
os.Exit(1)
}
if data[0] == '3' {
s = strings.TrimSpace(data[3:endHeader])
i++
continue
} else if data[0] != '2' {
fmt.Fprintf(os.Stderr, "Could not install file. Invalid server response. %s\n", err.Error())
os.Exit(1)
}
data = strings.TrimSpace(data[endHeader:])
break
}
return []byte(data)
}
func getGopher(s string) []byte {
u, err := url.Parse(s)
if err != nil {
fmt.Fprintf(os.Stderr, "Could not install file. Invalid URL. %s\n", err.Error())
os.Exit(1)
}
port := u.Port()
if port == "" {
port = "70"
}
c, err := net.Dial("tcp", u.Host+":"+port)
if err != nil {
fmt.Fprintf(os.Stderr, "Could not install file. Could not connect to server. %s\n", err.Error())
os.Exit(1)
}
defer c.Close()
c.Write([]byte(u.RawPath + "\r\n"))
data, err := io.ReadAll(c)
if err != nil {
fmt.Fprintf(os.Stderr, "Could not install file. Download failed. %s\n", err.Error())
os.Exit(1)
}
return data
}
func getHTTP(s string) []byte {
resp, err := http.Get(s)
if err != nil {
fmt.Fprintf(os.Stderr, "Could not install file. Download failed. %s\n", err.Error())
os.Exit(1)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Fprintf(os.Stderr, "Could not install file. Download failed. %s\n", err.Error())
os.Exit(1)
}
return body
}
func getLocalFile(s string) []byte {
p := expandedAbsFilepath(s)
if _, err := os.Stat(p); errors.Is(err, os.ErrNotExist) {
fmt.Fprint(os.Stderr, "Could not install file. Path does not exist\n")
os.Exit(1)
}
data, err := os.ReadFile(p)
if err != nil {
fmt.Fprintf(os.Stderr, "Could not install file. File read error: %s\n", err.Error())
os.Exit(1)
}
return data
}
func installModule(s string) {
lower := strings.ToLower(s)
if strings.HasPrefix(lower, "http://") || strings.HasPrefix(lower, "https://") {
writeLib(path.Base(s), getHTTP(s))
} else if strings.HasPrefix(lower, "gemini://") {
writeLib(path.Base(s), getGemini(s))
} else if strings.HasPrefix(lower, "gopher://") {
writeLib(path.Base(s), getGopher(s))
} else {
writeLib(path.Base(s), getLocalFile(s))
}
}