445 lines
9.0 KiB
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]
|
|
}
|