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) }