867 lines
19 KiB
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
|
|
}
|