felise/helpers.go

445 lines
9.0 KiB
Go

/*
Copyright (C) 2023 Brian Evans (aka sloum). All rights reserved.
This source code is available under the terms of the ffsl, or,
Floodgap Free Software License. A copy of the license has been
provided as the file 'LICENSE' in the same folder as this source
code file. If for some reason it is not present, you can find the
terms of version 1 of the FFSL at the following URL:
https://www.floodgap.com/software/ffsl/license.html
*/
package main
import (
"crypto/tls"
"fmt"
"io"
"math"
"net/url"
"strconv"
"strings"
"unicode"
)
func kindToString(k int) string {
switch k {
case INT:
return "INT"
case FLOAT:
return "FLOAT"
case STRING:
return "STRING"
case BOOL:
return "BOOL"
case SYMBOL:
return "SYMBOL"
case KEYWORD:
return "KEYWORD"
case TYPE:
return "TYPE"
case LIST:
return "LIST"
case DICT:
return "DICT"
case PROC:
return "PROC"
case WHILE:
return "WHILE"
case IF:
return "IF"
case DOCSTRING:
return "DOCSTRING"
case END:
return "."
default:
panic(fmt.Sprintf("Unknown item type received by kind: %d\n", k))
}
}
func typeInit(kind int) value {
switch kind {
case INT:
return 0
case FLOAT:
return 0.0
case BOOL:
return false
case STRING:
return ""
case LIST:
return list{make([]token, 0, 10)}
case DICT:
return dict{make(map[string]token)}
case TYPE:
return TYPE
case WHILE:
return while{}
case IF:
return condIf{}
case PROC:
return proc{}
case DOCSTRING:
return ""
default:
panic("Unknown type given to typeInit")
}
}
func isType(s string) (bool, int) {
switch s {
case "INT":
return true, INT
case "STRING":
return true, STRING
case "FLOAT":
return true, FLOAT
case "BOOL":
return true, BOOL
case "PROC":
return true, PROC
case "TYPE":
return true, TYPE
case "KEYWORD":
return true, KEYWORD
case "LIST":
return true, LIST
case "DICT":
return true, DICT
default:
return false, -1
}
}
func isKeyword(s string) bool {
switch s {
case "var!", "set!", "scoped-var!", "scoped-set!",
"+", "-", "*", "/", "proc", "proc!", "length", "return", "break",
"dup", "drop", "over", "swap", "cast",
"type", "while", "dowhile", "if", "else", "and", "or",
">", "<", "=", "stackdump", "clearstack", "->", "<-",
"=>", "->!", "=>!", "append", "append!", "list-get", "list-set",
"list-set!", "split", "join", "file-write", "file-append", "file-remove",
"file-exists?", "file-read", "docstring!", "input", "re-match?",
"re-find", "re-find-all", "re-replace", "slice", "stackdepth", "net-get", "try",
"catch", "throw", "import", "rot", "each!", "filter!", "words", "time",
"char-conv", "env-get", "env-set", "exec", "<-exec", "exec->", "<-exec->",
"cd", "pwd":
return true
default:
return false
}
}
func toString(t token, codeRepresentation bool) string {
switch t.kind {
case BOOL:
if t.val.(bool) {
return "true"
}
return "false"
case INT:
return strconv.Itoa(t.val.(int))
case TYPE:
return kindToString(t.val.(int))
case FLOAT:
return strconv.FormatFloat(t.val.(float64), 'f', -1, 64)
case KEYWORD, SYMBOL:
return t.val.(string)
case STRING:
if codeRepresentation {
return `"` + escapeString(t.val.(string)) + `"`
}
return t.val.(string)
case PROC:
if codeRepresentation {
return t.val.(proc).String()
}
return t.val.(proc).name
case LIST:
l := t.val.(list)
return l.String()
case DICT:
d := t.val.(dict)
return d.String()
case END:
return "."
default:
panic("Unknown type encountered in toString()\n")
}
}
func toBoolRaw(t token) bool {
switch t.kind {
case BOOL:
return t.val.(bool)
case INT:
return !(t.val.(int) == 0)
case STRING:
return !(t.val.(string) == "")
case FLOAT:
return !(t.val.(float64) == 0.0)
default:
return true
}
}
func toBool(t token) token {
switch t.kind {
case BOOL:
return t
case INT:
t.kind = BOOL
t.val = !(t.val.(int) == 0)
return t
case STRING:
t.kind = BOOL
t.val = !(t.val.(string) == "")
return t
case FLOAT:
return token{BOOL, !(t.val.(float64) == 0.0), t.line, t.file}
case LIST:
return token{BOOL, len(t.val.(list).body) > 0, t.line, t.file}
case DICT:
return token{BOOL, len(t.val.(dict).body) > 0, t.line, t.file}
default:
return token{BOOL, true, t.line, t.file}
}
}
func escapeString(s string) string {
var out strings.Builder
for _, c := range []rune(s) {
switch c {
case '\t':
out.WriteString("\\t")
case '\n':
out.WriteString("\\n")
case '\r':
out.WriteString("\\r")
case '\v':
out.WriteString("\\v")
case '\a':
out.WriteString("\\a")
case '\b':
out.WriteString("\\b")
case '\f':
out.WriteString("\\f")
case '\\':
out.WriteString("\\\\")
case '"':
out.WriteString(`\"`)
default:
if !unicode.IsPrint(c) {
out.WriteString(fmt.Sprintf("\\0x%X", c))
} else {
out.WriteRune(c)
}
}
}
return out.String()
}
func unescapeString(s string) string {
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 'a':
out.WriteRune('\a')
case 'b':
out.WriteRune('\b')
case '\\':
out.WriteRune('\\')
case 'f':
out.WriteRune('\f')
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 tokenStringToInt(tokenString string) (int, error) {
var i int64
var err error
if strings.HasPrefix(tokenString, "0x") {
i, err = strconv.ParseInt(tokenString[2:], 16, 0)
if err == nil {
return int(i), nil
}
}
// Check for octal string
if strings.HasPrefix(tokenString, "0o") {
i, err = strconv.ParseInt(tokenString[2:], 8, 0)
if err == nil {
return int(i), nil
}
}
// Check for binary string
if strings.HasPrefix(tokenString, "0b") {
i, err = strconv.ParseInt(tokenString[2:], 2, 0)
if err == nil {
return int(i), nil
}
}
i, err = strconv.ParseInt(tokenString, 10, 0)
if err == nil {
return int(i), nil
}
// If parsing as INT fails, try FLOAT then INT
y, err := strconv.ParseFloat(tokenString, 32)
if err == nil {
return int(math.Floor(y)), nil
}
return -1, fmt.Errorf("Could not convert STRING to INT")
}
func GeminiRequest(u *url.URL, redirectCount int) (int, string) {
if redirectCount >= 10 {
return 3, "Too many redirects"
}
if u.Port() == "" {
u.Host = u.Host + ":1965"
}
conf := &tls.Config{
MinVersion: tls.VersionTLS12,
InsecureSkipVerify: true,
}
conn, err := tls.Dial("tcp", u.Host, conf)
if err != nil {
return -1, err.Error()
}
defer conn.Close()
_, err = conn.Write([]byte(u.String() + "\r\n"))
if err != nil {
return -1, err.Error()
}
res, err := io.ReadAll(conn)
if err != nil {
return -1, err.Error()
}
resp := strings.SplitN(string(res), "\r\n", 2)
if len(resp) != 2 {
if err != nil {
return -1, "Invalid response from server"
}
}
header := strings.SplitN(resp[0], " ", 2)
if len([]rune(header[0])) != 2 {
header = strings.SplitN(resp[0], "\t", 2)
if len([]rune(header[0])) != 2 {
return -1, "Invalid response format from server"
}
}
// Get status code single digit form
status, err := strconv.Atoi(string(header[0][0]))
if err != nil {
return -1, "Invalid status response from server"
}
if status != 2 {
switch status {
case 1:
resp[1] = header[1]
case 3:
// This does not support relative redirects
// TODO add support
newUrl, err := url.Parse(header[1])
if err != nil {
resp[1] = "Redirect attempted to invalid URL"
break
}
return GeminiRequest(newUrl, redirectCount+1)
case 4:
resp[1] = fmt.Sprintf("Temporary failure; %s", header[1])
case 5:
resp[1] = fmt.Sprintf("Permanent failure; %s", header[1])
case 6:
resp[1] = "Client certificate required (unsupported by 'net-get')"
default:
resp[1] = "Invalid response status from server"
}
}
return status, resp[1]
}