diff options
| -rw-r--r-- | contrib/cgi/cgi.go | 16 | ||||
| -rw-r--r-- | contrib/cgi/gopher.go | 139 | ||||
| -rw-r--r-- | contrib/fs/gopher.go | 183 | ||||
| -rw-r--r-- | examples/gopher_fileserver/main.go | 11 | ||||
| -rw-r--r-- | gopher/gophermap/extended.go | 47 | ||||
| -rw-r--r-- | gopher/gophermap/extended_test.go | 6 | ||||
| -rw-r--r-- | gopher/gophermap/listdir.go | 91 | ||||
| -rw-r--r-- | gopher/gophermap/testdata/customlist_output.gophermap | 4 | ||||
| -rw-r--r-- | gopher/gophermap/testdata/file4.txt (renamed from gopher/gophermap/testdata/file4) | 0 | ||||
| -rw-r--r-- | gopher/gophermap/testdata/subdir/gophertag | 1 | ||||
| -rw-r--r-- | gopher/gophermap/testdata/subdir2/gophermap | 1 | ||||
| -rw-r--r-- | gopher/response.go | 2 | ||||
| -rw-r--r-- | internal/users.go | 53 | 
13 files changed, 396 insertions, 158 deletions
diff --git a/contrib/cgi/cgi.go b/contrib/cgi/cgi.go index bcdd5e1..749a284 100644 --- a/contrib/cgi/cgi.go +++ b/contrib/cgi/cgi.go @@ -25,11 +25,11 @@ import (  // a request for /foo/bar/baz can run an executable found at /foo or /foo/bar. In such  // a case the PATH_INFO would include the remaining portion of the URI path.  func ResolveCGI(requestPath, fsRoot string) (string, string, error) { +	fsRoot = strings.TrimRight(fsRoot, "/")  	segments := strings.Split(strings.TrimLeft(requestPath, "/"), "/")  	for i := range append(segments, "") {  		filepath := strings.Join(append([]string{fsRoot}, segments[:i]...), "/") -		filepath = strings.TrimRight(filepath, "/")  		isDir, isExecutable, err := executableFile(filepath)  		if err != nil {  			return "", "", err @@ -52,26 +52,20 @@ func ResolveCGI(requestPath, fsRoot string) (string, string, error) {  }  func executableFile(filepath string) (bool, bool, error) { -	file, err := os.Open(filepath) +	info, err := os.Stat(filepath)  	if isNotExistError(err) {  		return false, false, nil  	}  	if err != nil {  		return false, false, err  	} -	defer file.Close() - -	info, err := file.Stat() -	if err != nil { -		return false, false, err -	}  	if info.IsDir() {  		return true, false, nil  	}  	// readable + executable by anyone -	return false, info.Mode()&0005 == 0005, nil +	return false, info.Mode()&5 == 5, nil  }  func isNotExistError(err error) bool { @@ -94,7 +88,7 @@ func RunCGI(  	request *sr.Request,  	executable string,  	pathInfo string, -) (io.Reader, int, error) { +) (*bytes.Buffer, int, error) {  	pathSegments := strings.Split(executable, "/")  	dirPath := "." @@ -105,7 +99,7 @@ func RunCGI(  	infoLen := len(pathInfo)  	if pathInfo == "/" { -		infoLen -= 1 +		infoLen = 0  	}  	scriptName := request.Path[:len(request.Path)-infoLen] diff --git a/contrib/cgi/gopher.go b/contrib/cgi/gopher.go index 290adfa..98a3c75 100644 --- a/contrib/cgi/gopher.go +++ b/contrib/cgi/gopher.go @@ -3,10 +3,14 @@ package cgi  import (  	"context"  	"fmt" +	"os" +	"path" +	"path/filepath"  	"strings"  	sr "tildegit.org/tjp/sliderule"  	"tildegit.org/tjp/sliderule/gopher" +	"tildegit.org/tjp/sliderule/gopher/gophermap"  )  // GopherCGIDirectory runs any executable files relative to a root directory on the file system. @@ -15,31 +19,148 @@ import (  // a request for /foo/bar/baz can also run an executable found at /foo or  /foo/bar. In  // such a case the PATH_INFO environment variable will include the remaining portion of  // the URI path. -func GopherCGIDirectory(pathRoot, fsRoot string) sr.Handler { +func GopherCGIDirectory(pathRoot, fsRoot string, settings *gophermap.FileSystemSettings) sr.Handler { +	if settings == nil { +		settings = &gophermap.FileSystemSettings{} +	} + +	if !settings.Exec { +		return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response { return nil }) +	} +  	fsRoot = strings.TrimRight(fsRoot, "/")  	return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response {  		if !strings.HasPrefix(request.Path, pathRoot) {  			return nil  		} -		filepath, pathinfo, err := ResolveCGI(request.Path[len(pathRoot):], fsRoot) +		fullpath, pathinfo, err := resolveGopherCGI(fsRoot, request)  		if err != nil {  			return gopher.Error(err).Response()  		} -		if filepath == "" { +		if fullpath == "" { +			return nil +		} + +		return runGopherCGI(ctx, request, fullpath, pathinfo, *settings) +	}) +} + +// ExecGopherMaps runs any gophermaps +func ExecGopherMaps(pathRoot, fsRoot string, settings *gophermap.FileSystemSettings) sr.Handler { +	if settings == nil { +		settings = &gophermap.FileSystemSettings{} +	} + +	if !settings.Exec { +		return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response { return nil }) +	} + +	fsRoot = strings.TrimRight(fsRoot, "/") +	return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response { +		if !strings.HasPrefix(request.Path, pathRoot) {  			return nil  		} -		stdout, exitCode, err := RunCGI(ctx, request, filepath, pathinfo) +		fullpath := filepath.Join(fsRoot, strings.Trim(request.Path, "/")) +		info, err := os.Stat(fullpath) +		if isNotExistError(err) { +			return nil +		}  		if err != nil {  			return gopher.Error(err).Response()  		} -		if exitCode != 0 { -			return gopher.Error( -				fmt.Errorf("CGI process exited with status %d", exitCode), -			).Response() + +		if info.IsDir() { +			for _, fname := range settings.DirMaps { +				fpath := filepath.Join(fullpath, fname) +				finfo, err := os.Stat(fpath) +				if isNotExistError(err) { +					continue +				} +				if err != nil { +					return gopher.Error(err).Response() +				} + +				m := finfo.Mode() +				if m.IsDir() { +					continue +				} +				if !m.IsRegular() || m&5 != 5 { +					continue +				} +				return runGopherCGI(ctx, request, fpath, "/", *settings) +			} + +			return nil +		} + +		m := info.Mode() +		if !m.IsRegular() || m&5 != 5 { +			return nil  		} -		return gopher.File(0, stdout) +		return runGopherCGI(ctx, request, fullpath, "/", *settings)  	})  } + +func runGopherCGI( +	ctx context.Context, +	request *sr.Request, +	fullpath string, +	pathinfo string, +	settings gophermap.FileSystemSettings, +) *sr.Response { +	stdout, exitCode, err := RunCGI(ctx, request, fullpath, pathinfo) +	if err != nil { +		return gopher.Error(err).Response() +	} +	if exitCode != 0 { +		return gopher.Error( +			fmt.Errorf("CGI process exited with status %d", exitCode), +		).Response() +	} + +	if settings.ParseExtended { +		edoc, err := gophermap.ParseExtended(stdout, request.URL) +		if err != nil { +			return gopher.Error(err).Response() +		} + +		doc, _, err := edoc.Compatible(filepath.Dir(fullpath), settings) +		return doc.Response() +	} + +	return gopher.File(gopher.MenuType, stdout) +} + +func resolveGopherCGI(fsRoot string, request *sr.Request) (string, string, error) { +	reqPath := strings.TrimLeft(request.Path, "/") + +	segments := append([]string{""}, strings.Split(reqPath, "/")...) +	fullpath := fsRoot +	for i, segment := range segments { +		fullpath = filepath.Join(fullpath, segment) + +		info, err := os.Stat(fullpath) +		if isNotExistError(err) { +			return "", "", nil +		} +		if err != nil { +			return "", "", err +		} + +		if !info.IsDir() { +			if info.Mode()&5 == 5 { +				pathinfo := "/" +				if len(segments) > i+1 { +					pathinfo = path.Join(segments[i:]...) +				} +				return fullpath, pathinfo, nil +			} +			break +		} +	} + +	return "", "", nil +} diff --git a/contrib/fs/gopher.go b/contrib/fs/gopher.go index 0594730..4d86ba6 100644 --- a/contrib/fs/gopher.go +++ b/contrib/fs/gopher.go @@ -2,29 +2,56 @@ package fs  import (  	"context" -	"io/fs" +	"os" +	"path/filepath" +	"slices"  	"strings" -	"text/template"  	sr "tildegit.org/tjp/sliderule"  	"tildegit.org/tjp/sliderule/gopher" +	"tildegit.org/tjp/sliderule/gopher/gophermap"  )  // GopherFileHandler builds a handler which serves up files from a file system.  //  // It only serves responses for paths which correspond to files, not directories. -func GopherFileHandler(fileSystem fs.FS) sr.Handler { +func GopherFileHandler(rootpath string, settings *gophermap.FileSystemSettings) sr.Handler {  	return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response { -		filepath, file, err := ResolveFile(request, fileSystem) +		path := filepath.Join(rootpath, strings.Trim(request.Path, "/")) +		if isPrivate(path) { +			return nil +		} +		if isf, err := isFile(path); err != nil { +			return gopher.Error(err).Response() +		} else if !isf { +			return nil +		} + +		if settings == nil { +			settings = &gophermap.FileSystemSettings{} +		} + +		file, err := os.Open(path)  		if err != nil {  			return gopher.Error(err).Response()  		} -		if file == nil { -			return nil +		if !(settings.ParseExtended && isMap(path, *settings)) { +			return gopher.File(gopher.GuessItemType(path), file) +		} + +		defer func() { _ = file.Close() }() + +		edoc, err := gophermap.ParseExtended(file, request.URL) +		if err != nil { +			return gopher.Error(err).Response()  		} -		return gopher.File(gopher.GuessItemType(filepath), file) +		doc, _, err := edoc.Compatible(filepath.Dir(path), *settings) +		if err != nil { +			return gopher.Error(err).Response() +		} +		return doc.Response()  	})  } @@ -34,95 +61,111 @@ func GopherFileHandler(fileSystem fs.FS) sr.Handler {  // contents of that file is returned as the gopher response.  //  // It returns nil for any paths which don't correspond to a directory. -// -// It requires that files from the provided fs.FS implement fs.ReadDirFile. If -// they don't, it will produce nil responses for all directory paths. -func GopherDirectoryDefault(fileSystem fs.FS, filenames ...string) sr.Handler { +func GopherDirectoryDefault(rootpath string, settings *gophermap.FileSystemSettings) sr.Handler {  	return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response { -		dirpath, dir, err := ResolveDirectory(request, fileSystem) -		if err != nil { -			return gopher.Error(err).Response() +		path := filepath.Join(rootpath, strings.Trim(request.Path, "/")) +		if isPrivate(path) { +			return nil  		} -		if dir == nil { +		if isd, err := isDir(path); err != nil { +			return gopher.Error(err).Response() +		} else if !isd {  			return nil  		} -		defer func() { _ = dir.Close() }() -		_, file, err := ResolveDirectoryDefault(fileSystem, dirpath, dir, filenames) -		if err != nil { -			return gopher.Error(err).Response() +		if settings == nil { +			settings = &gophermap.FileSystemSettings{}  		} -		if file == nil { -			return nil + +		for _, fname := range settings.DirMaps { +			fpath := filepath.Join(path, fname) +			if isf, err := isFile(fpath); err != nil { +				return gopher.Error(err).Response() +			} else if !isf { +				continue +			} + +			file, err := os.Open(fpath) +			if err != nil { +				return gopher.Error(err).Response() +			} + +			if settings.ParseExtended { +				defer func() { _ = file.Close() }() + +				edoc, err := gophermap.ParseExtended(file, request.URL) +				if err != nil { +					return gopher.Error(err).Response() +				} + +				doc, _, err := edoc.Compatible(path, *settings) +				if err != nil { +					return gopher.Error(err).Response() +				} +				return doc.Response() +			} else { +				return gopher.File(gopher.MenuType, file) +			}  		} -		return gopher.File(gopher.MenuType, file) +		return nil  	})  }  // GopherDirectoryListing produces a listing of the contents of any requested directories.  //  // It returns nil for any paths which don't correspond to a filesystem directory. -// -// It requires that files from the provided fs.FS implement fs.ReadDirFile. If they -// don't, it will produce nil responses for any directory paths. -// -// A template may be nil, in which case DefaultGopherDirectoryList is used instead. The -// template is then processed with RenderDirectoryListing. -func GopherDirectoryListing(fileSystem fs.FS, tpl *template.Template) sr.Handler { +func GopherDirectoryListing(rootpath string, settings *gophermap.FileSystemSettings) sr.Handler {  	return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response { -		dirpath, dir, err := ResolveDirectory(request, fileSystem) -		if err != nil { -			return gopher.Error(err).Response() +		path := filepath.Join(rootpath, strings.Trim(request.Path, "/")) +		if isPrivate(path) { +			return nil  		} -		if dir == nil { +		if isd, err := isDir(path); err != nil { +			return gopher.Error(err).Response() +		} else if !isd {  			return nil  		} -		defer func() { _ = dir.Close() }() -		if tpl == nil { -			tpl = DefaultGopherDirectoryList +		if settings == nil { +			settings = &gophermap.FileSystemSettings{}  		} -		body, err := RenderDirectoryListing(dirpath, dir, tpl, request.Server) +		doc, err := gophermap.ListDir(path, request.URL, *settings)  		if err != nil {  			return gopher.Error(err).Response()  		} -		return gopher.File(gopher.MenuType, body) +		return doc.Response()  	})  } -// GopherTemplateFunctions is a map for templates providing useful functions for gophermaps. -// -// - GuessItemType: return a gopher item type for a file based on its path/name. -var GopherTemplateFunctions = template.FuncMap{ -	"GuessItemType": func(filepath string) string { -		return string([]byte{byte(gopher.GuessItemType(filepath))}) -	}, +func isDir(path string) (bool, error) { +	info, err := os.Stat(path) +	if err != nil { +		if isNotFound(err) { +			err = nil +		} +		return false, err +	} +	return info.IsDir() && info.Mode()&4 == 4, nil  } -// DefaultGopherDirectoryList is a template which renders a directory listing as gophermap. -var DefaultGopherDirectoryList = template.Must( -	template.New("gopher_dirlist").Funcs(GopherTemplateFunctions).Parse( -		strings.ReplaceAll( -			` -{{ $root := .FullPath -}} -{{ if eq .FullPath "." }}{{ $root = "" }}{{ end -}} -{{ $hostname := .Hostname -}} -{{ $port := .Port -}} -i{{ .DirName }}		{{ $hostname }}	{{ $port }} -i		{{ $hostname }}	{{ $port }} -{{ range .Entries -}} -{{ if .IsDir -}} -1{{ .Name }}	{{ $root }}/{{ .Name }}	{{ $hostname }}	{{ $port }} -{{- else -}} -{{ GuessItemType .Name }}{{ .Name }}	{{ $root }}/{{ .Name }}	{{ $hostname }}	{{ $port }} -{{- end }} -{{ end -}} -. -`[1:], -			"\n", -			"\r\n", -		), -	), -) +func isFile(path string) (bool, error) { +	info, err := os.Stat(path) +	if err != nil { +		if isNotFound(err) { +			err = nil +		} +		return false, err +	} +	m := info.Mode() + +	return m.IsRegular() && m&4 == 4, nil +} + +func isMap(path string, settings gophermap.FileSystemSettings) bool { +	if strings.HasSuffix(path, ".gophermap") { +		return true +	} +	return slices.Contains(settings.DirMaps, filepath.Base(path)) +} diff --git a/examples/gopher_fileserver/main.go b/examples/gopher_fileserver/main.go index 244f61b..057aa21 100644 --- a/examples/gopher_fileserver/main.go +++ b/examples/gopher_fileserver/main.go @@ -3,7 +3,6 @@ package main  import (  	"context"  	"log" -	"os"  	sr "tildegit.org/tjp/sliderule"  	"tildegit.org/tjp/sliderule/contrib/cgi" @@ -13,13 +12,11 @@ import (  )  func main() { -	fileSystem := os.DirFS(".") -  	handler := sr.FallthroughHandler( -		fs.GopherDirectoryDefault(fileSystem, "index.gophermap"), -		fs.GopherDirectoryListing(fileSystem, nil), -		cgi.GopherCGIDirectory("/cgi-bin", "./cgi-bin"), -		fs.GopherFileHandler(fileSystem), +		fs.GopherDirectoryDefault(".", nil), +		fs.GopherDirectoryListing(".", nil), +		cgi.GopherCGIDirectory("/cgi-bin", "./cgi-bin", nil), +		fs.GopherFileHandler(".", nil),  	)  	_, infoLog, _, errLog := logging.DefaultLoggers() diff --git a/gopher/gophermap/extended.go b/gopher/gophermap/extended.go index d9fedd0..a0360fc 100644 --- a/gopher/gophermap/extended.go +++ b/gopher/gophermap/extended.go @@ -7,10 +7,12 @@ import (  	"net/url"  	"os"  	"path/filepath" +	"sort"  	"strconv"  	"strings"  	"tildegit.org/tjp/sliderule/gopher" +	"tildegit.org/tjp/sliderule/internal"  	"tildegit.org/tjp/sliderule/internal/types"  ) @@ -139,6 +141,8 @@ const (  	UserListType types.Status = '~'  	// VHostListType generates a listing of virtual hosts. +	// +	// It is not supported in sliderule.  	VHostListType types.Status = '%'  	// InclusionType causes another gophermap to be included at this location. @@ -151,8 +155,16 @@ const (  	EndDocType types.Status = '.'  ) +type FileSystemSettings struct { +	ParseExtended bool +	Exec          bool +	ListUsers     bool +	DirMaps       []string +	DirTag        string +} +  // Compatible builds a standards-compliant gophermap from the current extended menu. -func (edoc ExtendedMapDocument) Compatible(cwd string) (gopher.MapDocument, string, error) { +func (edoc ExtendedMapDocument) Compatible(cwd string, settings FileSystemSettings) (gopher.MapDocument, string, error) {  	doc := gopher.MapDocument{}  	title := "" @@ -172,9 +184,32 @@ func (edoc ExtendedMapDocument) Compatible(cwd string) (gopher.MapDocument, stri  				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 +		case UserListType: +			if !settings.ListUsers { +				doc = append(doc, gopher.MapItem{ +					Type:     gopher.InfoMessageType, +					Display:  "~", +					Selector: edoc.Location.Path, +					Hostname: edoc.Location.Hostname(), +					Port:     edoc.Location.Port(), +				}) +			} + +			users, err := internal.ListUsersWithHomeSubdir("public_gopher", 4) +			if err != nil { +				return nil, "", err +			} +			sort.Strings(users) +			for _, user := range users { +				doc = append(doc, gopher.MapItem{ +					Type:     gopher.MenuType, +					Display:  "~" + user, +					Selector: "/~" + user, +					Hostname: edoc.Location.Hostname(), +					Port:     edoc.Location.Port(), +				}) +			} +		case VHostListType:  			return nil, "", errors.New("Virtual host listings '%' are not supported")  		case InclusionType:  			location := filepath.Join(cwd, item.Selector) @@ -183,13 +218,13 @@ func (edoc ExtendedMapDocument) Compatible(cwd string) (gopher.MapDocument, stri  				return nil, "", err  			} -			lines, _, err := subEdoc.Compatible(filepath.Dir(location)) +			lines, _, err := subEdoc.Compatible(filepath.Dir(location), settings)  			if err != nil {  				return nil, "", err  			}  			doc = append(doc, lines...)  		case DirListType: -			dirlist, err := listDir(cwd, edoc.Location, hidden, extensions) +			dirlist, err := listDir(cwd, edoc.Location, settings, hidden, extensions)  			if err != nil {  				return nil, "", err  			} diff --git a/gopher/gophermap/extended_test.go b/gopher/gophermap/extended_test.go index e956df1..2d9e9b1 100644 --- a/gopher/gophermap/extended_test.go +++ b/gopher/gophermap/extended_test.go @@ -31,7 +31,11 @@ func TestExtendedDoc(t *testing.T) {  		t.Fatal(err)  	} -	doc, _, err := edoc.Compatible("testdata") +	doc, _, err := edoc.Compatible("testdata", FileSystemSettings{ +		ParseExtended: true, +		DirMaps:       []string{"gophermap"}, +		DirTag:        "gophertag", +	})  	if err != nil {  		t.Fatal(err)  	} diff --git a/gopher/gophermap/listdir.go b/gopher/gophermap/listdir.go index 8d66277..a2c5214 100644 --- a/gopher/gophermap/listdir.go +++ b/gopher/gophermap/listdir.go @@ -6,6 +6,7 @@ import (  	"os"  	"path"  	"path/filepath" +	"slices"  	"strings"  	"tildegit.org/tjp/sliderule/gopher" @@ -13,11 +14,11 @@ import (  )  // 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, settings FileSystemSettings) (gopher.MapDocument, error) { +	return listDir(dir, location, settings, nil, nil)  } -func listDir(dir string, location *url.URL, hidden map[string]struct{}, extensions map[string]types.Status) (gopher.MapDocument, error) { +func listDir(dir string, location *url.URL, settings FileSystemSettings, hidden map[string]struct{}, extensions map[string]types.Status) (gopher.MapDocument, error) {  	contents, err := os.ReadDir(dir)  	if err != nil {  		return nil, err @@ -28,55 +29,36 @@ func listDir(dir string, location *url.URL, hidden map[string]struct{}, extensio  	for _, entry := range contents {  		name := entry.Name() -		if _, ok := hidden[name]; ok || name == "gophermap" { +		inf, err := entry.Info() +		if err != nil { +			return nil, err +		} +		if inf.Mode()&4 == 0 { +			continue +		} + +		if _, ok := hidden[name]; ok || slices.Contains(settings.DirMaps, name) {  			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 +			ext := strings.TrimPrefix(filepath.Ext(name), ".") +			if c, ok := extensions[ext]; ok { +				code = c +			} else if c, ok := extensions[name]; ok { +				code = c +			} else { +				code = gopher.GuessItemType(name)  			}  		}  		doc = append(doc, gopher.MapItem{  			Type:     code, -			Display:  displayName(dir, entry), +			Display:  displayName(dir, entry, settings),  			Selector: path.Join(path.Dir(location.Path), name),  			Hostname: location.Hostname(),  			Port:     location.Port(), @@ -86,32 +68,37 @@ func listDir(dir string, location *url.URL, hidden map[string]struct{}, extensio  	return doc, nil  } -func displayName(dir string, entry os.DirEntry) string { +func displayName(dir string, entry os.DirEntry, settings FileSystemSettings) string {  	fname := entry.Name() +	fullpath := filepath.Join(dir, fname) -	// if is a gophermap, use !title or filename -	if strings.HasSuffix(fname, ".gophermap") { -		if title := gophermapTitle(dir, fname); title != "" { +	if entry.Type().IsRegular() && settings.ParseExtended && (strings.HasSuffix(fname, ".gophermap") || slices.Contains(settings.DirMaps, fname)) { +		if title := gophermapTitle(fullpath); title != "" {  			return title  		} -		return fname  	}  	if entry.IsDir() { -		if tag := tagTitle(dir, fname); tag != "" { -			return tag +		if settings.DirTag != "" { +			if tag := tagTitle(filepath.Join(fullpath, settings.DirTag)); tag != "" { +				return tag +			}  		} -		if title := gophermapTitle(dir, filepath.Join(fname, "gophermap")); title != "" { -			return title +		if settings.ParseExtended { +			for _, mapname := range settings.DirMaps { +				if title := gophermapTitle(filepath.Join(fullpath, mapname)); title != "" { +					return title +				} +			}  		}  	}  	return fname  } -func gophermapTitle(dir, name string) string { -	file, err := os.Open(filepath.Join(dir, name)) +func gophermapTitle(path string) string { +	file, err := os.Open(path)  	if err != nil {  		return ""  	} @@ -131,8 +118,8 @@ func gophermapTitle(dir, name string) string {  	return strings.TrimRight(line[1:], "\r\n")  } -func tagTitle(parent, name string) string { -	file, err := os.Open(filepath.Join(parent, name, "gophertag")) +func tagTitle(path string) string { +	file, err := os.Open(path)  	if err != nil {  		return ""  	} diff --git a/gopher/gophermap/testdata/customlist_output.gophermap b/gopher/gophermap/testdata/customlist_output.gophermap index 1a96644..e5cc99d 100644 --- a/gopher/gophermap/testdata/customlist_output.gophermap +++ b/gopher/gophermap/testdata/customlist_output.gophermap @@ -8,5 +8,7 @@ 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
 +0file4.txt	/file4.txt	localhost.localdomain	70
 +1subdir title	/subdir	localhost.localdomain	70
 +1subdir2 title	/subdir2	localhost.localdomain	70
  .
 diff --git a/gopher/gophermap/testdata/file4 b/gopher/gophermap/testdata/file4.txt index e69de29..e69de29 100644 --- a/gopher/gophermap/testdata/file4 +++ b/gopher/gophermap/testdata/file4.txt diff --git a/gopher/gophermap/testdata/subdir/gophertag b/gopher/gophermap/testdata/subdir/gophertag new file mode 100644 index 0000000..73a248e --- /dev/null +++ b/gopher/gophermap/testdata/subdir/gophertag @@ -0,0 +1 @@ +subdir title diff --git a/gopher/gophermap/testdata/subdir2/gophermap b/gopher/gophermap/testdata/subdir2/gophermap new file mode 100644 index 0000000..bba9538 --- /dev/null +++ b/gopher/gophermap/testdata/subdir2/gophermap @@ -0,0 +1 @@ +!subdir2 title diff --git a/gopher/response.go b/gopher/response.go index 1ad7f1d..46f9db0 100644 --- a/gopher/response.go +++ b/gopher/response.go @@ -121,7 +121,7 @@ func Error(err error) *MapItem {  // File builds a minimal response delivering a file's contents.  // -// Meta is nil and Status is 0 in this response. +// Meta is nil in this response.  func File(status types.Status, contents io.Reader) *types.Response {  	return &types.Response{Status: status, Body: contents}  } diff --git a/internal/users.go b/internal/users.go new file mode 100644 index 0000000..e7a25a3 --- /dev/null +++ b/internal/users.go @@ -0,0 +1,53 @@ +package internal + +import ( +	"bufio" +	"errors" +	"io" +	"os" +	"path/filepath" +	"strings" +) + +// ListUsersWithHomeSubdir provides a list of user names on the current host. +// +// The users returned will only be those with a specifically named subdirectory +// of their home dir, which has the given permission bits. +func ListUsersWithHomeSubdir(subdir string, required_perms os.FileMode) ([]string, error) { +	file, err := os.Open("/etc/passwd") +	if err != nil { +		return nil, err +	} +	defer func() { +		_ = file.Close() +	}() + +	users := []string{} +	rdr := bufio.NewReader(file) +	for { +		line, err := rdr.ReadString('\n') +		isEOF := errors.Is(err, io.EOF) +		if err != nil && !isEOF { +			return nil, err +		} + +		if len(line) != 0 && line[0] == '#' { +			continue +		} + +		spl := strings.Split(line, ":") +		home := spl[len(spl)-2] +		st, err := os.Stat(filepath.Join(home, subdir)) +		if err != nil || !st.IsDir() || st.Mode()&required_perms != required_perms { +			continue +		} + +		users = append(users, spl[0]) + +		if isEOF { +			break +		} +	} + +	return users, nil +}  | 
