@ 261101.7
current
package main

import (
	"bytes"
	"errors"
	"fmt"
	"html/template"
	"io"
	"io/fs"
	"net/http"
	"net/url"
	"path"
	"slices"
	"strings"
	"unicode/utf8"
)

// maxCode caps the size of a file shown as a code page; larger files go raw.
const maxCode = 1 << 20

// name is the file below the tree that urlPath names, or "." for its root.
func (t *tree) name(urlPath string) string {
	rest := strings.TrimPrefix(urlPath, t.prefix)
	name := strings.TrimPrefix(path.Clean("/"+rest), "/")
	if name == "" {
		return "."
	}
	return name
}

func (t *tree) serve(w http.ResponseWriter, r *http.Request) {
	if first, rest, ok := t.versionIn(r.URL.Path); ok {
		t.serveVersion(w, r, first, rest)
		return
	}
	name := t.name(r.URL.Path)
	// A first segment such as @5430ac3 named a version before versions
	// moved beside the tree's name, unless the tree holds an entry by that
	// name. It moves there for good, so old links still lead to it.
	if first, rest, _ := strings.Cut(name, "/"); t.hasVersions() && atSegment.MatchString(first) {
		if _, err := fs.Stat(t.fsys, first); err != nil {
			if t.prefix == "/" {
				t.serveVersion(w, r, first, rest)
				return
			}
			u := (&url.URL{Path: versionPrefix(t.prefix, first) + rest}).String()
			if strings.HasSuffix(r.URL.Path, "/") && rest != "" {
				u += "/"
			}
			if r.URL.RawQuery != "" {
				u += "?" + r.URL.RawQuery
			}
			http.Redirect(w, r, u, http.StatusMovedPermanently)
			return
		}
	}
	info, err := fs.Stat(t.fsys, name)
	if ref := (*refusal)(nil); errors.As(err, &ref) {
		t.fail(w, r, name, http.StatusForbidden, t.why(ref))
		return
	}
	if err != nil {
		t.notFound(w, r, name)
		return
	}
	if info.IsDir() && !strings.HasSuffix(r.URL.Path, "/") {
		// Without the slash, relative links in the listing resolve one level up.
		u := r.URL.EscapedPath() + "/"
		if r.URL.RawQuery != "" {
			u += "?" + r.URL.RawQuery
		}
		http.Redirect(w, r, u, http.StatusMovedPermanently)
		return
	}
	if q, ok := r.URL.Query()["search"]; ok && info.IsDir() {
		t.serveSearch(w, r, name, q[0])
		return
	}
	if _, ok := r.URL.Query()["backlinks"]; ok && !info.IsDir() {
		t.serveBacklinks(w, r, name)
		return
	}
	if _, log := r.URL.Query()["log"]; log {
		t.serveLog(w, r, name, info.IsDir())
		return
	}
	if _, ok := r.URL.Query()["timeline"]; ok {
		t.serveTimeline(w, r, name, info.IsDir())
		return
	}
	if hash, ok := r.URL.Query()["commit"]; ok {
		if q := r.URL.Query(); q.Has("decl") {
			t.serveDecl(w, r, name, info.IsDir(), hash[0], q.Get("file"), q.Get("decl"))
			return
		}
		t.serveCommit(w, r, name, info.IsDir(), hash[0])
		return
	}
	if info.IsDir() {
		t.serveDir(w, r, name)
		return
	}
	if _, raw := r.URL.Query()["raw"]; raw {
		t.serveRaw(w, r, name, info)
		return
	}
	switch {
	case isMarkdown(name):
		t.serveMarkdown(w, r, name)
	case info.Size() <= maxCode:
		t.serveCode(w, r, name, info)
	default:
		t.serveRaw(w, r, name, info)
	}
}

func isMarkdown(name string) bool {
	switch strings.ToLower(path.Ext(name)) {
	case ".md", ".markdown":
		return true
	}
	return false
}

func isText(b []byte) bool {
	return utf8.Valid(b) && bytes.IndexByte(b, 0) < 0
}

func (t *tree) serveMarkdown(w http.ResponseWriter, r *http.Request, name string) {
	b, err := fs.ReadFile(t.fsys, name)
	if err != nil {
		t.fail(w, r, name, http.StatusInternalServerError, err.Error())
		return
	}
	body, err := renderMarkdown(b, t.missing(name), t.webBase())
	if err != nil {
		t.fail(w, r, name, http.StatusInternalServerError, err.Error())
		return
	}
	body = template.HTML(t.logLine(r.Context(), name, b)) + body
	t.render(w, r, http.StatusOK, page{Title: name, Crumbs: t.crumbs(name), Kind: "doc", Body: body, Raw: true})
}

// webBase is where t's docs send a link to a forge: the web address, ""
// on the web address itself, or nil where no server here serves it, eg on
// a share.
func (t *tree) webBase() *string {
	if t.base != nil {
		return t.base.webBase()
	}
	switch s := t.srv; {
	case s == nil:
		return nil
	case s.web != nil:
		return new(string)
	case s.webURL != "":
		return &s.webURL
	}
	return nil
}

// missing reports, for the doc name, whether a relative link names
// nothing the reader could open. A link that climbs out of the tree may
// land in another tree, so it counts as found.
func (t *tree) missing(name string) func(string) bool {
	dir := path.Dir(name)
	return func(dest string) bool {
		target := path.Join(dir, dest)
		if target == ".." || strings.HasPrefix(target, "../") {
			return false
		}
		_, err := fs.Stat(t.fsys, target)
		return errors.Is(err, fs.ErrNotExist)
	}
}

// serveCode shows a text file as a highlighted code page with numbered,
// linkable lines; ?scopes draws each block of code as a box, ?tree
// shows the syntax tree, and ?gron shows a JSON file as one line per node.
// Binary files go raw.
func (t *tree) serveCode(w http.ResponseWriter, r *http.Request, name string, info fs.FileInfo) {
	b, err := fs.ReadFile(t.fsys, name)
	if err != nil {
		t.fail(w, r, name, http.StatusInternalServerError, err.Error())
		return
	}
	if !isText(b) {
		t.serveRaw(w, r, name, info)
		return
	}
	var body strings.Builder
	body.WriteString(t.logLine(r.Context(), name, b))
	// The links to the other views and to the raw file float over the
	// code's top right corner; div.code bounds how far they follow the
	// page down. A tree that cannot be had says why above the flat code.
	q := r.URL.Query()
	_, scoped := q["scopes"]
	if _, ok := q["gron"]; ok && isJSON(name) {
		out, err := gron(b)
		if err == nil {
			body.WriteString(`<div class="code">` + codeViews(name, "gron") + highlightPage(gronLexer, out, false) + `</div>`)
			t.render(w, r, http.StatusOK, page{Title: name, Crumbs: t.crumbs(name), Kind: "code", Body: template.HTML(body.String())})
			return
		}
		body.WriteString("<p class=\"own warn\">No gron view, since this is not valid JSON: " + template.HTMLEscapeString(err.Error()) + "</p>\n")
	}
	if _, ok := q["tree"]; ok {
		tree, err := syntaxPage(r.Context(), name, pageHref(r.URL.Path), b)
		if err == nil {
			// A large file's tree is megabytes, so it is written once,
			// into room made for it.
			body.Grow(len(tree) + 1024)
			body.WriteString(`<div class="code">` + codeViews(name, "tree"))
			body.WriteString(tree)
			body.WriteString(`</div>`)
			t.render(w, r, http.StatusOK, page{Title: name, Crumbs: t.crumbs(name), Kind: "tree", Body: template.HTML(body.String())})
			return
		}
		logErr("no syntax tree for %s: %v", name, err)
		body.WriteString(treeNote(name, err))
	}
	view := "flat"
	if scoped {
		view = "scopes"
	}
	body.WriteString(`<div class="code">` + codeViews(name, view))
	body.WriteString(highlightPage(codeLexer(name), b, scoped))
	body.WriteString(`</div>`)
	t.render(w, r, http.StatusOK, page{Title: name, Crumbs: t.crumbs(name), Kind: "code", Body: template.HTML(body.String())})
}

// serveRaw sends the file itself. Text goes as plain text, so a stray .html
// or .ts file shows as source instead of rendering or downloading.
func (t *tree) serveRaw(w http.ResponseWriter, r *http.Request, name string, info fs.FileInfo) {
	f, err := t.fsys.Open(name)
	if err != nil {
		t.notFound(w, r, name)
		return
	}
	defer f.Close()
	rs, ok := f.(io.ReadSeeker)
	if !ok {
		t.fail(w, r, name, http.StatusInternalServerError, "file is not seekable")
		return
	}
	if strings.ToLower(path.Ext(name)) != ".svg" {
		head := make([]byte, 512)
		n, _ := io.ReadFull(rs, head)
		if isText(head[:n]) {
			w.Header().Set("Content-Type", "text/plain; charset=utf-8")
		}
		rs.Seek(0, io.SeekStart)
	}
	http.ServeContent(w, r, name, info.ModTime(), rs)
}

func (t *tree) serveDir(w http.ResponseWriter, r *http.Request, name string) {
	entries, err := fs.ReadDir(t.fsys, name)
	if err != nil {
		t.fail(w, r, name, http.StatusInternalServerError, err.Error())
		return
	}
	type item struct {
		name string
		dir  bool
		ref  *refusal
	}
	// Entries reached through a link out of the tree are checked one by
	// one, since each may be refused. So is every entry of a shared tree.
	// A version holds only what git stored, so nothing leads out of it.
	outside := false
	if t.at == nil {
		_, err = t.root.Stat(name)
		outside = err != nil || t.shared
	}
	var items []item
	for _, e := range entries {
		n := e.Name()
		if t.unlisted(name, n) {
			continue
		}
		dir := e.IsDir()
		var ref *refusal
		if e.Type()&fs.ModeSymlink != 0 || outside {
			ref = t.refused(path.Join(name, n))
			if ref != nil {
				if ref.link == path.Join(name, n) {
					dir = ref.dir
				}
			} else if info, err := fs.Stat(t.fsys, path.Join(name, n)); err == nil {
				dir = info.IsDir()
			}
		}
		items = append(items, item{n, dir, ref})
	}
	slices.SortStableFunc(items, func(a, b item) int {
		if a.dir != b.dir {
			if a.dir {
				return -1
			}
			return 1
		}
		return strings.Compare(a.name, b.name)
	})

	var body strings.Builder
	if t.at != nil {
		body.WriteString(t.versionLine(r.Context(), name, true))
	}
	body.WriteString("<ul class=\"listing\">\n")
	if t.lastCommit(r.Context(), name) != nil {
		body.WriteString("<li class=\"log\"><a href=\"?log\">" + template.HTMLEscapeString(logLabel) + "</a></li>\n")
	}
	if name != "." {
		body.WriteString("<li><a href=\"../\">../</a></li>\n")
	}
	for _, it := range items {
		label := it.name
		if it.dir {
			label += "/"
		}
		href := (&url.URL{Path: label}).String()
		if it.ref != nil && (t.shared || t.public) {
			fmt.Fprintf(&body, "<li class=\"refused\">%s <span class=\"own\">refused</span></li>\n", template.HTMLEscapeString(label))
			continue
		}
		if it.ref != nil {
			fmt.Fprintf(&body, "<li class=\"refused\"><a href=\"%s\" title=\"%s\">%s</a> <span class=\"own\">refused</span></li>\n",
				template.HTMLEscapeString(href), template.HTMLEscapeString(it.ref.Error()), template.HTMLEscapeString(label))
			continue
		}
		fmt.Fprintf(&body, "<li><a href=\"%s\">%s</a></li>\n",
			template.HTMLEscapeString(href), template.HTMLEscapeString(label))
	}
	body.WriteString("</ul>\n")
	t.render(w, r, http.StatusOK, page{
		Title:  t.title(name),
		Crumbs: t.crumbs(name),
		Kind:   "dir",
		Body:   template.HTML(body.String()),
	})
}