slope/helpers.go

867 lines
19 KiB
Go

package main
import (
"crypto/tls"
"fmt"
"io"
"io/ioutil"
"net"
"net/url"
"os"
"os/user"
"path/filepath"
"strconv"
"strings"
"syscall"
"unicode"
)
func AnythingToBool(e expression) expression {
switch i := e.(type) {
case bool:
return i
default:
return true
}
}
func MergeSort(slice []expression, sublistIndex int) []expression {
if len(slice) < 2 {
return slice
}
mid := (len(slice)) / 2
return Merge(MergeSort(slice[:mid], sublistIndex), MergeSort(slice[mid:], sublistIndex), sublistIndex)
}
func Merge(left, right []expression, ind int) []expression {
size, i, j := len(left)+len(right), 0, 0
slice := make([]expression, size, size)
if ind >= 0 {
for k := 0; k < size; k++ {
if i > len(left)-1 && j <= len(right)-1 {
slice[k] = right[j]
j++
continue
} else if j > len(right)-1 && i <= len(left)-1 {
slice[k] = left[i]
i++
continue
}
i1, i1IsNumber := left[i].([]expression)[ind].(number)
i2, i2IsNumber := right[j].([]expression)[ind].(number)
if (i1IsNumber && i2IsNumber && i1 < i2) || (i1IsNumber && !i2IsNumber) {
slice[k] = left[i]
i++
} else if !i1IsNumber && i2IsNumber {
slice[k] = right[j]
j++
} else if (!i1IsNumber && !i2IsNumber) && String(left[i].([]expression)[ind], false) < String(right[j].([]expression)[ind], false) {
slice[k] = left[i]
i++
} else {
slice[k] = right[j]
j++
}
}
} else {
for k := 0; k < size; k++ {
if i > len(left)-1 && j <= len(right)-1 {
slice[k] = right[j]
j++
continue
} else if j > len(right)-1 && i <= len(left)-1 {
slice[k] = left[i]
i++
continue
}
i1, i1IsNumber := left[i].(number)
i2, i2IsNumber := right[j].(number)
if (i1IsNumber && i2IsNumber && float64(i1) < float64(i2)) || (!i1IsNumber && i2IsNumber) {
slice[k] = left[i]
i++
} else if i1IsNumber && !i2IsNumber {
slice[k] = right[j]
j++
} else if (!i1IsNumber && !i2IsNumber) && String(left[i], false) < String(right[j], false) {
slice[k] = left[i]
i++
} else {
slice[k] = right[j]
j++
}
}
}
return slice
}
func MergeLists(a ...expression) (expression, error) {
length := -1
for i := range a[0].([]expression) {
if _, ok := a[0].([]expression)[i].([]expression); !ok {
return 0, fmt.Errorf("a value other than a list was provided")
}
if length == -1 {
length = len(a[0].([]expression)[i].([]expression))
} else if length != len(a[0].([]expression)[i].([]expression)) {
return 0, fmt.Errorf("lists are of unequal length")
}
}
mergedLists := make([]expression, 0, len(a[0].([]expression)[0].([]expression)))
for i, _ := range a[0].([]expression)[0].([]expression) {
lineList := make([]expression, 0, len(a[0].([]expression)))
for l, _ := range a[0].([]expression) {
lineList = append(lineList, a[0].([]expression)[l].([]expression)[i])
}
mergedLists = append(mergedLists, lineList)
}
return mergedLists, nil
}
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("\\\\")
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 SafeExit(code int) {
for i := range openFiles {
if !openFiles[i].Open {
continue
}
switch o := openFiles[i].Obj.(type) {
case *os.File:
o.Close()
openFiles[i].Open = false
case *net.Conn:
(*o).Close()
openFiles[i].Open = false
case *tls.Conn:
o.Close()
openFiles[i].Open = false
}
}
histFile := ExpandedAbsFilepath(filepath.Join(getModBaseDir(), "..", historyFilename))
if f, e := os.Create(histFile); e == nil && line != nil {
line.WriteHistory(f)
f.Close()
}
if linerTerm != nil || initialTerm != nil {
line.Close()
}
// NOTE: This was removed as it was breaking pipelines
// leaving this commented line in until the removal
// is vetted as not causing other problems
// termios.Restore()
os.Exit(code)
}
func StringSliceToExpressionSlice(s []string) expression {
e := make([]expression, len(s))
for i := range s {
e[i] = s[i]
}
return e
}
func ExpressionSliceToStringSlice(e []expression) []string {
s := make([]string, len(e))
for i := range s {
s[i] = String(e[i], false)
}
return s
}
func loadFiles(files []expression) {
for _, v := range files {
err := RunFile(ExpandedAbsFilepath(v.(string)), true)
if err != nil {
panic(err.Error())
}
}
}
type Module struct {
text string
notes []string
dependencies []string
description string
source string
entry string
}
func ParseModFile(p string) (Module, error) {
b, err := ioutil.ReadFile(p)
if err != nil {
return Module{}, fmt.Errorf("Could not load modfile: %s", p)
}
s := string(b)
lines := strings.Split(s, "\n")
var mod Module
mod.text = s
for _, line := range lines {
fields := strings.SplitN(line, " ", 2)
if len(fields) != 2 {
continue
}
fields[1] = strings.TrimSpace(fields[1])
switch strings.ToLower(fields[0]) {
case "entry":
mod.entry = fields[1]
case "source":
mod.source = fields[1]
default:
continue
}
}
if mod.entry == "" && mod.source == "" {
return mod, fmt.Errorf("Modfile missing entry and source fields")
} else if mod.entry == "" {
return mod, fmt.Errorf("Modfile missing entry field")
} else if mod.source == "" {
return mod, fmt.Errorf("Modfile missing source field")
}
return mod, nil
}
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 handleSignals(c <-chan os.Signal) {
for {
switch <-c {
case syscall.SIGINT:
apply(globalenv.vars["__SIGINT"], make([]expression, 0), "__SIGINT")
}
}
}
func formatValue(format string, value expression) (string, error) {
v := String(value, false)
left := false
count := 0
if len(format) > 0 {
i, err := strconv.Atoi(format)
if err != nil {
return v, err
}
if i < 0 {
left = true
i = i * -1
}
count = i
}
if count <= len([]rune(v)) {
return v, nil
}
if left {
return v + strings.Repeat(" ", count-len(v)), nil
}
return strings.Repeat(" ", count-len(v)) + v, nil
}
func getModBaseDir() string {
p := os.Getenv("SLOPE_MOD_PATH")
if p == "" {
x := os.Getenv("XDG_DATA_HOME")
if x == "" {
return ExpandedAbsFilepath("~/.local/share/slope/modules/")
}
return filepath.Join(x, "slope", "modules")
}
return p
}
func createDataDirs(p string) {
p = ExpandedAbsFilepath(p)
_, err := os.Stat(p)
if os.IsNotExist(err) {
os.MkdirAll(p, 0755)
}
}
func getPreloadDir() string {
p := os.Getenv("SLOPE_PRELOAD_DIR")
if p == "" {
x := os.Getenv("XDG_DATA_HOME")
if x == "" {
return ExpandedAbsFilepath("~/.local/share/slope/preload/")
}
return filepath.Join(x, "slope", "preload")
}
return p
}
func preloadFiles() {
files, err := filepath.Glob(filepath.Join(PreloadDir, "*.slo"))
if err != nil {
fmt.Fprint(os.Stderr, "Could not preload files. Reading preload directory failed\n")
}
exp := make([]expression, len(files))
for i := range files {
f, err := os.Stat(files[i])
if err != nil || f.IsDir() {
continue
}
exp[i] = filepath.Join(files[i])
}
loadFiles(exp)
}
func percentToDate(c rune) string {
switch c {
case 'a':
return "pm" // 12 hour segment lowercase
case 'A':
return "PM" // 12 hour segment uppercase
case 'd':
return "2" // Day, no leading zero
case 'D':
return "02" // Day, leading zero
case 'e':
return "_2" // Day, no leading zero, with space padding
case 'f':
return "Jan" // Full month, short
case 'F':
return "January" // Full month, long
case 'g', 'G':
return "15" // Hour, 24 hour format
case 'h':
return "3" // Hour, 12 hour format, no leading zero
case 'H':
return "03" // Hour, 12 hour format w/ leading zero
case 'i':
return "4" // Minutes, no leading zero
case 'I':
return "04" // Minutes, leading zero
case 'm':
return "1" // Month number, no leading zero
case 'M':
return "01" // Month number, leading zero
case 'o':
return "-07" // Timezone offset, only hours
case 'O':
return "-0700" // Timezone offset, hours and minutes
case 's':
return "5" // Seconds, no leading zero
case 'S':
return "05" // Seconds, leading zero
case 'w':
return "Mon" // Weekday, short
case 'W':
return "Monday" // Weekday, long
case 'y':
return "06" // Year, two digit
case 'Y':
return "2006" // Year, four digit
case 'Z':
return "MST" // Time zone as three chars
case '%':
return "%" // Literal percent
default:
return "?" // Unknown escape sequence
}
}
func createTimeFormatString(s string) string {
var out strings.Builder
r := strings.NewReader(s)
for {
c, count, err := r.ReadRune()
if err != nil || count == 0 {
break
}
switch c {
case '%':
c, count, err = r.ReadRune()
if err != nil || count == 0 {
out.WriteString(percentToDate('%'))
break
}
out.WriteString(percentToDate(c))
default:
out.WriteRune(c)
}
}
return out.String()
}
// Used by REPL to know if another input line
// should be offered before parsing
func stringParensMatch(s string) (bool, bool, bool) {
count := 0
inString := false
prevPrev := rune(0)
inrawString := false
prev := ' '
for _, c := range s {
switch c {
case '(':
if !inString && !inrawString && prev != '\'' {
count++
}
case ')':
if !inString && !inrawString {
count--
}
case '`':
if inString {
break
}
if !inrawString {
inrawString = true
count++
} else if prev != '\\' || (prev == '\\' && prevPrev == '\\') {
inrawString = false
count--
}
case '"':
if inrawString {
break
}
if !inString {
inString = true
} else if prev != '\\' || (prev == '\\' && prevPrev == '\\') {
inString = false
}
}
prevPrev = prev
prev = c
}
if inString {
return (count == 0), inrawString, true
}
if count > 0 {
return false, inrawString, false
}
// If count is negative still return true, the
// parser will handle erroring
return true, inrawString, false
}
func variadic(l []expression) int {
args := len(l)
variadic := false
for _, v := range l {
if arg, ok := v.(symbol); ok && (arg == symbol("args-list") || arg == symbol("...")) {
variadic = true
}
if variadic {
args -= 1
}
}
return args
}
func completeFromMap(m map[string]string, input string, index int) (c []string) {
for k, _ := range m {
if index < 0 && strings.HasPrefix(string(k), input) {
c = append(c, string(k))
} else if len(input) > index+1 && strings.HasPrefix(string(k), input[index+1:]) {
start := len(input) - index - 1
if start < 0 {
start = 0
}
c = append(c, input+string(k)[start:])
}
}
return
}
func addGUIToLib() {
for k, v := range guiLib {
stdLibrary[k] = v
}
for k, v := range guiUsageStrings {
usageStrings[k] = v
}
}
func addClipToLib() {
for k, v := range clipLib {
stdLibrary[k] = v
}
for k, v := range clipUsageStrings {
usageStrings[k] = v
}
}
func addDialogToLib() {
for k, v := range dialogLib {
stdLibrary[k] = v
}
for k, v := range dialogUsageStrings {
usageStrings[k] = v
}
}
func SysoutPrint(val, io expression) {
stringOut := String(val, false)
obj, ok := io.(*IOHandle)
if !ok {
panic("runtime exception: tried to print to a non-writable io-handle")
}
if !obj.Open {
panic("runtime exception: tried to print to a non-writable io-handle")
}
switch ft := obj.Obj.(type) {
case *os.File:
ft.WriteString(stringOut)
case *net.Conn:
(*ft).Write([]byte(stringOut))
case *tls.Conn:
ft.Write([]byte(stringOut))
case *strings.Builder:
ft.WriteString(stringOut)
default:
panic("runtime exception: tried to print to a non-writable io-handle")
}
}
// https://github.com/yargevad/filepathx/blob/master/filepathx.go
// Globs represents one filepath glob, with its elements joined by "**".
type Globs []string
// BetterGlob adds double-star support to the core path/filepath Glob function.
func BetterGlob(pattern string) ([]string, error) {
if !strings.Contains(pattern, "**") {
// passthru to core package if no double-star
return filepath.Glob(pattern)
}
return Globs(strings.Split(pattern, "**")).Expand()
}
// Expand finds matches for the provided Globs.
func (globs Globs) Expand() ([]string, error) {
var matches = []string{""} // accumulate here
for _, glob := range globs {
var hits []string
var hitMap = map[string]bool{}
for _, match := range matches {
paths, err := filepath.Glob(match + glob)
if err != nil {
return nil, err
}
for _, path := range paths {
err = filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// save deduped match from current iteration
if _, ok := hitMap[path]; !ok {
hits = append(hits, path)
hitMap[path] = true
}
return nil
})
if err != nil {
return nil, err
}
}
}
matches = hits
}
// fix up return value for nil input
if globs == nil && len(matches) > 0 && matches[0] == "" {
matches = matches[1:]
}
return matches, nil
}
func GetUsageMap(modName string) (map[string]string, error) {
modEnv, ok := namespaces[modName]
if !ok {
return map[string]string{}, fmt.Errorf("module %s does not exist", modName)
}
usage, ok := modEnv.vars[symbol("_USAGE")]
if !ok {
return map[string]string{}, fmt.Errorf("module %s does not share usage information", modName)
}
data := make(map[string]string)
list, ok := usage.([]expression)
if !ok {
return map[string]string{}, fmt.Errorf("module %s shares malformed usage data, an assoc was expected but not given", modName)
}
for _, v := range list {
pair, ok := v.([]expression)
if !ok {
continue
}
if len(pair) > 1 {
data[String(pair[0], false)] = String(pair[1], false)
}
}
return data, nil
}
func getAllModFuncNames() map[string]string {
out := make(map[string]string)
inverse := make(map[string]string)
for k, v := range altnamespaces {
inverse[v] = k
}
for k := range namespaces {
alt, ok := inverse[k]
m, err := GetUsageMap(k)
if err != nil {
continue
}
for name := range m {
if ok {
out[fmt.Sprintf("%s::%s", alt, name)] = ""
} else {
out[fmt.Sprintf("%s::%s", k, name)] = ""
}
}
}
return out
}
func ByteSliceToExpressionSlice(b []byte) []expression {
out := make([]expression, len(b))
for i := range b {
out[i] = number(b[i])
}
return out
}
func DeepCopySlice(s []expression) expression {
clone := make([]expression, len(s))
copy(clone, s)
for k, v := range clone {
if slice, ok := v.([]expression); ok {
clone[k] = DeepCopySlice(slice)
}
}
return clone
}
func GeminiRequest(u *url.URL, redirectCount int) (string, string, error) {
if redirectCount >= 10 {
return "", "", fmt.Errorf("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 "", "", err
}
defer conn.Close()
_, err = conn.Write([]byte(u.String() + "\r\n"))
if err != nil {
return "", "", err
}
res, err := io.ReadAll(conn)
if err != nil {
return "", "", err
}
resp := strings.SplitN(string(res), "\r\n", 2)
if len(resp) != 2 {
if err != nil {
return "", "", fmt.Errorf("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 "", "", fmt.Errorf("Invalid response format from server")
}
}
// Get status code single digit form
status, err := strconv.Atoi(string(header[0][0]))
if err != nil {
return "", "", fmt.Errorf("Invalid status response from server")
}
if status != 2 {
switch status {
case 1:
return header[1], "", nil
case 3:
newUrl, err := url.Parse(header[1])
if err != nil {
return "", "", fmt.Errorf("Redirect attempted to invalid URL")
}
return GeminiRequest(newUrl, redirectCount+1)
case 4:
return "", "", fmt.Errorf("Temporary failure; %s", header[1])
case 5:
return "", "", fmt.Errorf("Permanent failure; %s", header[1])
case 6:
return "", "", fmt.Errorf("Client certificate required (unsupported by 'net-get')")
default:
return "", "", fmt.Errorf("Invalid response status from server")
}
}
return resp[1], header[1], nil
}