summaryrefslogtreecommitdiff
path: root/gopher
diff options
context:
space:
mode:
authortjpcc <tjp@ctrl-c.club>2023-09-03 19:58:18 -0600
committertjpcc <tjp@ctrl-c.club>2023-09-03 19:58:18 -0600
commit9a68591255747c82fd4ce99351bca6d43349cafa (patch)
tree06928eeb617b954a80bbdab2e6164952d47e13bf /gopher
parent5befdc9c851f285000c15abc01a08010c719b307 (diff)
implement gophernicus extensions for gophermaps
Diffstat (limited to 'gopher')
-rw-r--r--gopher/gophermap/extended.go220
-rw-r--r--gopher/gophermap/extended_test.go57
-rw-r--r--gopher/gophermap/listdir.go154
-rw-r--r--gopher/gophermap/parse.go4
-rw-r--r--gopher/gophermap/testdata/customlist.gophermap15
-rw-r--r--gopher/gophermap/testdata/customlist_output.gophermap12
-rw-r--r--gopher/gophermap/testdata/file10
-rw-r--r--gopher/gophermap/testdata/file20
-rw-r--r--gopher/gophermap/testdata/file3.executablething0
-rw-r--r--gopher/gophermap/testdata/file40
10 files changed, 462 insertions, 0 deletions
diff --git a/gopher/gophermap/extended.go b/gopher/gophermap/extended.go
new file mode 100644
index 0000000..d9fedd0
--- /dev/null
+++ b/gopher/gophermap/extended.go
@@ -0,0 +1,220 @@
+package gophermap
+
+import (
+ "bufio"
+ "errors"
+ "io"
+ "net/url"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+
+ "tildegit.org/tjp/sliderule/gopher"
+ "tildegit.org/tjp/sliderule/internal/types"
+)
+
+// ExtendedMapDocument is a gophermap doc with gophernicus's extensions
+//
+// These are documented at: gopher://gopher.gophernicus.org/0/docs/README.Gophermap
+type ExtendedMapDocument struct {
+ Lines []gopher.MapItem
+ Location *url.URL
+}
+
+// ParseExtended parses a gophermap document including gophernicus extensions.
+func ParseExtended(input io.Reader, location *url.URL) (ExtendedMapDocument, error) {
+ rdr := bufio.NewReader(input)
+ doc := ExtendedMapDocument{Location: location}
+
+outer:
+ for num := 1; ; num += 1 {
+ line, err := rdr.ReadString('\n')
+ isEOF := errors.Is(err, io.EOF)
+ if err != nil && !isEOF {
+ return doc, err
+ }
+ line = strings.TrimRight(line, "\r\n")
+
+ if len(line) > 0 {
+ switch line[0] {
+
+ case '#':
+ doc.Lines = append(doc.Lines, gopher.MapItem{
+ Type: CommentType,
+ Display: strings.TrimPrefix(line[1:], " "),
+ })
+ continue outer
+ case '!':
+ doc.Lines = append(doc.Lines, gopher.MapItem{
+ Type: TitleType,
+ Display: line[1:],
+ })
+ continue outer
+ case '-':
+ doc.Lines = append(doc.Lines, gopher.MapItem{
+ Type: HiddenType,
+ Selector: line[1:],
+ })
+ continue outer
+ case ':':
+ doc.Lines = append(doc.Lines, gopher.MapItem{
+ Type: ExtensionType,
+ Display: line[1:],
+ })
+ continue outer
+ case '=':
+ doc.Lines = append(doc.Lines, gopher.MapItem{
+ Type: InclusionType,
+ Selector: line[1:],
+ })
+ continue outer
+ }
+ }
+
+ switch line {
+ case "~":
+ doc.Lines = append(doc.Lines, gopher.MapItem{Type: UserListType})
+ continue outer
+ case "%":
+ doc.Lines = append(doc.Lines, gopher.MapItem{Type: VHostListType})
+ continue outer
+ case ".":
+ doc.Lines = append(doc.Lines, gopher.MapItem{Type: EndDocType})
+ break outer
+ case "*":
+ doc.Lines = append(doc.Lines, gopher.MapItem{Type: DirListType})
+ break outer
+ }
+
+ if !strings.Contains(line, "\t") {
+ doc.Lines = append(doc.Lines, gopher.MapItem{
+ Type: gopher.InfoMessageType,
+ Display: line,
+ Selector: location.Path,
+ Hostname: location.Hostname(),
+ Port: location.Port(),
+ })
+ continue
+ }
+
+ item := gopher.MapItem{Type: types.Status(line[0])}
+
+ spl := strings.Split(line[1:], "\t")
+ if len(spl) != 4 {
+ return doc, InvalidLine(num)
+ }
+ item.Display = string(spl[0])
+ item.Selector = string(spl[1])
+ item.Hostname = string(spl[2])
+ item.Port = string(spl[3])
+ if _, err = strconv.Atoi(item.Port); err != nil {
+ return doc, InvalidLine(num)
+ }
+ doc.Lines = append(doc.Lines, item)
+
+ if isEOF {
+ break
+ }
+ }
+
+ return doc, nil
+}
+
+// Extensions to gopher types from Gophernicus.
+const (
+ // CommentType is omitted from generated compatible gophermaps.
+ CommentType types.Status = '#'
+
+ // TitleType defines the title of a gophermap document.
+ TitleType types.Status = '!'
+
+ // HiddenType hides a link from the generated compatible gophermap.
+ HiddenType types.Status = '-'
+
+ // ExtensionType defines the gopher type to use for files in the current directory with a given extension.
+ ExtensionType types.Status = ':'
+
+ // UserListType generates a list of users with valid ~/public_gopher directories.
+ UserListType types.Status = '~'
+
+ // VHostListType generates a listing of virtual hosts.
+ VHostListType types.Status = '%'
+
+ // InclusionType causes another gophermap to be included at this location.
+ InclusionType types.Status = '='
+
+ // DirListType stops parsing the current file and ends the generated gophermap with a listing of the current directory.
+ DirListType types.Status = '*'
+
+ // EndDocType ends the current gophermap file.
+ EndDocType types.Status = '.'
+)
+
+// Compatible builds a standards-compliant gophermap from the current extended menu.
+func (edoc ExtendedMapDocument) Compatible(cwd string) (gopher.MapDocument, string, error) {
+ doc := gopher.MapDocument{}
+
+ title := ""
+ hidden := map[string]struct{}{}
+ extensions := map[string]types.Status{}
+
+ for num, item := range edoc.Lines {
+ switch item.Type {
+ case CommentType:
+ case TitleType:
+ title = item.Display
+ case HiddenType:
+ hidden[item.Selector] = struct{}{}
+ case ExtensionType:
+ from, to, found := strings.Cut(item.Display, "=")
+ if !found {
+ return nil, "", InvalidLine(num)
+ }
+ extensions[from] = types.Status(to[0])
+ case UserListType: //TODO
+ return nil, "", errors.New("User listings '~' are not supported")
+ case VHostListType: //TODO
+ return nil, "", errors.New("Virtual host listings '%' are not supported")
+ case InclusionType:
+ location := filepath.Join(cwd, item.Selector)
+ subEdoc, err := openExtended(location, edoc.Location)
+ if err != nil {
+ return nil, "", err
+ }
+
+ lines, _, err := subEdoc.Compatible(filepath.Dir(location))
+ if err != nil {
+ return nil, "", err
+ }
+ doc = append(doc, lines...)
+ case DirListType:
+ dirlist, err := listDir(cwd, edoc.Location, hidden, extensions)
+ if err != nil {
+ return nil, "", err
+ }
+
+ doc = append(doc, dirlist...)
+
+ break
+ case EndDocType:
+ break
+ default:
+ doc = append(doc, item)
+ }
+ }
+
+ return doc, title, nil
+}
+
+func openExtended(path string, location *url.URL) (ExtendedMapDocument, error) {
+ file, err := os.Open(path)
+ if err != nil {
+ return ExtendedMapDocument{}, err
+ }
+ defer func() {
+ _ = file.Close()
+ }()
+
+ return ParseExtended(file, location)
+}
diff --git a/gopher/gophermap/extended_test.go b/gopher/gophermap/extended_test.go
new file mode 100644
index 0000000..e956df1
--- /dev/null
+++ b/gopher/gophermap/extended_test.go
@@ -0,0 +1,57 @@
+package gophermap
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "net/url"
+ "os"
+ "testing"
+)
+
+func TestExtendedDoc(t *testing.T) {
+ file, err := os.Open("testdata/customlist.gophermap")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ source := &bytes.Buffer{}
+ _, err = io.Copy(source, file)
+ _ = file.Close()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ edoc, err := ParseExtended(source, &url.URL{
+ Scheme: "gopher",
+ Host: "localhost.localdomain:70",
+ Path: "/customlist.gophermap",
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ doc, _, err := edoc.Compatible("testdata")
+ if err != nil {
+ t.Fatal(err)
+ }
+ got := doc.String()
+
+ file, err = os.Open("testdata/customlist_output.gophermap")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ expected := &bytes.Buffer{}
+ _, err = io.Copy(expected, file)
+ _ = file.Close()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if expected.String() != got {
+ fmt.Printf("expected:\n%s", expected.String())
+ fmt.Printf("got:\n%s", got)
+ t.Fatal("output mismatch")
+ }
+}
diff --git a/gopher/gophermap/listdir.go b/gopher/gophermap/listdir.go
new file mode 100644
index 0000000..8d66277
--- /dev/null
+++ b/gopher/gophermap/listdir.go
@@ -0,0 +1,154 @@
+package gophermap
+
+import (
+ "bufio"
+ "net/url"
+ "os"
+ "path"
+ "path/filepath"
+ "strings"
+
+ "tildegit.org/tjp/sliderule/gopher"
+ "tildegit.org/tjp/sliderule/internal/types"
+)
+
+// ListDir builds a gopher menu representing the contents of a directory.
+func ListDir(dir string, location *url.URL) (gopher.MapDocument, error) {
+ return listDir(dir, location, nil, nil)
+}
+
+func listDir(dir string, location *url.URL, hidden map[string]struct{}, extensions map[string]types.Status) (gopher.MapDocument, error) {
+ contents, err := os.ReadDir(dir)
+ if err != nil {
+ return nil, err
+ }
+
+ doc := gopher.MapDocument{}
+
+ for _, entry := range contents {
+ name := entry.Name()
+
+ if _, ok := hidden[name]; ok || name == "gophermap" {
+ continue
+ }
+
+ var code types.Status
+
+ ext := strings.TrimPrefix(filepath.Ext(name), ".")
+ if entry.IsDir() {
+ code = gopher.MenuType
+ } else if c, ok := extensions[ext]; ok && ext != "" {
+ code = c
+ } else {
+ switch ext {
+ case "gophermap":
+ code = gopher.MenuType
+ case "exe", "bin", "out":
+ code = gopher.BinaryFileType
+ case "gif":
+ code = gopher.GifFileType
+ case "jpg", "jpeg", "tif", "tiff":
+ code = gopher.ImageFileType
+ case "bmp":
+ code = gopher.BitmapType
+ case "mp4", "mov", "avi", "wmv", "webm":
+ code = gopher.MovieFileType
+ case "pcm", "aiff", "mp3", "aac", "ogg", "wma", "flac", "alac":
+ code = gopher.SoundFileType
+ case "doc", "docx", "odt", "fodt":
+ code = gopher.DocumentType
+ case "html":
+ code = gopher.HTMLType
+ case "png":
+ code = gopher.PngImageFileType
+ case "rtf":
+ code = gopher.RtfDocumentType
+ case "wav":
+ code = gopher.WavSoundFileType
+ case "pdf":
+ code = gopher.PdfDocumentType
+ case "xml", "atom":
+ code = gopher.XmlDocumentType
+ default:
+ code = gopher.TextFileType
+ }
+ }
+
+ doc = append(doc, gopher.MapItem{
+ Type: code,
+ Display: displayName(dir, entry),
+ Selector: path.Join(path.Dir(location.Path), name),
+ Hostname: location.Hostname(),
+ Port: location.Port(),
+ })
+ }
+
+ return doc, nil
+}
+
+func displayName(dir string, entry os.DirEntry) string {
+ fname := entry.Name()
+
+ // if is a gophermap, use !title or filename
+ if strings.HasSuffix(fname, ".gophermap") {
+ if title := gophermapTitle(dir, fname); title != "" {
+ return title
+ }
+ return fname
+ }
+
+ if entry.IsDir() {
+ if tag := tagTitle(dir, fname); tag != "" {
+ return tag
+ }
+
+ if title := gophermapTitle(dir, filepath.Join(fname, "gophermap")); title != "" {
+ return title
+ }
+ }
+
+ return fname
+}
+
+func gophermapTitle(dir, name string) string {
+ file, err := os.Open(filepath.Join(dir, name))
+ if err != nil {
+ return ""
+ }
+ defer func() {
+ _ = file.Close()
+ }()
+
+ rdr := bufio.NewReader(file)
+ line, err := rdr.ReadString('\n')
+ if err != nil {
+ return ""
+ }
+
+ if !strings.HasPrefix(line, "!") {
+ return ""
+ }
+ return strings.TrimRight(line[1:], "\r\n")
+}
+
+func tagTitle(parent, name string) string {
+ file, err := os.Open(filepath.Join(parent, name, "gophertag"))
+ if err != nil {
+ return ""
+ }
+ defer func() {
+ _ = file.Close()
+ }()
+
+ stat, err := file.Stat()
+ if err != nil || stat.IsDir() {
+ return ""
+ }
+
+ rdr := bufio.NewReader(file)
+ line, err := rdr.ReadString('\n')
+ if err != nil {
+ return ""
+ }
+ return strings.TrimRight(line, "\r\n")
+}
diff --git a/gopher/gophermap/parse.go b/gopher/gophermap/parse.go
index 3317514..04286bd 100644
--- a/gopher/gophermap/parse.go
+++ b/gopher/gophermap/parse.go
@@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"io"
+ "strconv"
sr "tildegit.org/tjp/sliderule"
"tildegit.org/tjp/sliderule/gopher"
@@ -38,6 +39,9 @@ func Parse(input io.Reader) (gopher.MapDocument, error) {
item.Selector = string(spl[1])
item.Hostname = string(spl[2])
item.Port = string(spl[3])
+ if _, err = strconv.Atoi(item.Port); err != nil {
+ return nil, InvalidLine(num)
+ }
doc = append(doc, item)
}
diff --git a/gopher/gophermap/testdata/customlist.gophermap b/gopher/gophermap/testdata/customlist.gophermap
new file mode 100644
index 0000000..0d0c882
--- /dev/null
+++ b/gopher/gophermap/testdata/customlist.gophermap
@@ -0,0 +1,15 @@
+!This is the document title
+
+These should be info-message lines since they
+have no tab characters anywhere.
+
+# This is a comment!
+
+-file1
+-file2
+
+:executablething=9
+
+*
+
+this is unreachable
diff --git a/gopher/gophermap/testdata/customlist_output.gophermap b/gopher/gophermap/testdata/customlist_output.gophermap
new file mode 100644
index 0000000..1a96644
--- /dev/null
+++ b/gopher/gophermap/testdata/customlist_output.gophermap
@@ -0,0 +1,12 @@
+i /customlist.gophermap localhost.localdomain 70
+iThese should be info-message lines since they /customlist.gophermap localhost.localdomain 70
+ihave no tab characters anywhere. /customlist.gophermap localhost.localdomain 70
+i /customlist.gophermap localhost.localdomain 70
+i /customlist.gophermap localhost.localdomain 70
+i /customlist.gophermap localhost.localdomain 70
+i /customlist.gophermap localhost.localdomain 70
+1This is the document title /customlist.gophermap localhost.localdomain 70
+1customlist_output.gophermap /customlist_output.gophermap localhost.localdomain 70
+9file3.executablething /file3.executablething localhost.localdomain 70
+0file4 /file4 localhost.localdomain 70
+.
diff --git a/gopher/gophermap/testdata/file1 b/gopher/gophermap/testdata/file1
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gopher/gophermap/testdata/file1
diff --git a/gopher/gophermap/testdata/file2 b/gopher/gophermap/testdata/file2
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gopher/gophermap/testdata/file2
diff --git a/gopher/gophermap/testdata/file3.executablething b/gopher/gophermap/testdata/file3.executablething
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gopher/gophermap/testdata/file3.executablething
diff --git a/gopher/gophermap/testdata/file4 b/gopher/gophermap/testdata/file4
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gopher/gophermap/testdata/file4