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 }