Lindenii Project Forge
Login

server

Lindenii Forge’s main backend daemon
Commit info
ID
320d25d2c43929ddcc25cb22979bbdd3aaffd3b7
Author
Runxi Yu <me@runxiyu.org>
Author date
Sat, 05 Apr 2025 14:07:58 +0800
Committer
Runxi Yu <me@runxiyu.org>
Committer date
Sat, 05 Apr 2025 14:07:58 +0800
Actions
Reduce allocations when converting string to []byte
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>

package main

import (
	"encoding/hex"
	"errors"
	"fmt"
	"io"
	"net"
	"net/http"
	"strings"

	"git.sr.ht/~sircmpwn/go-bare"
)

type commitDisplay struct {
	Hash    string
	Author  string
	Email   string
	Date    string
	Message string
}

// httpHandleRepoIndex provides the front page of a repo using git2d.
func httpHandleRepoIndex(w http.ResponseWriter, req *http.Request, params map[string]any) {
	repoName := params["repo_name"].(string)
	groupPath := params["group_path"].([]string)

	_, repoPath, _, _, _, _, _ := getRepoInfo(req.Context(), groupPath, repoName, "") // TODO: Don't use getRepoInfo

	var notes []string
	if strings.Contains(repoName, "\n") || sliceContainsNewlines(groupPath) {
		notes = append(notes, "Path contains newlines; HTTP Git access impossible")
	}

	conn, err := net.Dial("unix", config.Git.Socket)
	if err != nil {
		errorPage500(w, params, "git2d connection failed: "+err.Error())
		return
	}
	defer conn.Close()

	writer := bare.NewWriter(conn)
	reader := bare.NewReader(conn)

	if err := writer.WriteData([]byte(repoPath)); err != nil {
	if err := writer.WriteData(stringToBytes(repoPath)); err != nil {
		errorPage500(w, params, "sending repo path failed: "+err.Error())
		return
	}

	if err := writer.WriteUint(1); err != nil {
		errorPage500(w, params, "sending command failed: "+err.Error())
		return
	}

	status, err := reader.ReadUint()
	if err != nil {
		errorPage500(w, params, "reading status failed: "+err.Error())
		return
	}
	if status != 0 {
		errorPage500(w, params, fmt.Sprintf("git2d error: %d", status))
		return
	}

	// README
	readmeRaw, err := reader.ReadData()
	if err != nil {
		readmeRaw = nil
	}
	readmeFilename, readmeRendered := renderReadme(readmeRaw, "README.md")

	// Commits
	var commits []commitDisplay
	for {
		id, err := reader.ReadData()
		if err != nil {
			if errors.Is(err, io.EOF) {
				break
			}
			errorPage500(w, params, "error reading commit ID: "+err.Error())
			return
		}

		title, _ := reader.ReadData()
		authorName, _ := reader.ReadData()
		authorEmail, _ := reader.ReadData()
		authorDate, _ := reader.ReadData()

		commits = append(commits, commitDisplay{
			Hash:    hex.EncodeToString(id),
			Author:  string(authorName),
			Email:   string(authorEmail),
			Date:    string(authorDate),
			Message: string(title),
		})
	}

	params["commits"] = commits
	params["readme_filename"] = readmeFilename
	params["readme"] = readmeRendered
	params["notes"] = notes

	renderTemplate(w, "repo_index", params)

	// TODO: Caching
}
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>

package main

import (
	"errors"
	"fmt"
	"html/template"
	"io"
	"net"
	"net/http"
	"strings"

	"git.sr.ht/~sircmpwn/go-bare"
)

// httpHandleRepoRaw serves raw files, or directory listings that point to raw
// files.
func httpHandleRepoRaw(writer http.ResponseWriter, request *http.Request, params map[string]any) {
	repoName := params["repo_name"].(string)
	groupPath := params["group_path"].([]string)
	rawPathSpec := params["rest"].(string)
	pathSpec := strings.TrimSuffix(rawPathSpec, "/")
	params["path_spec"] = pathSpec

	_, repoPath, _, _, _, _, _ := getRepoInfo(request.Context(), groupPath, repoName, "")

	conn, err := net.Dial("unix", config.Git.Socket)
	if err != nil {
		errorPage500(writer, params, "git2d connection failed: "+err.Error())
		return
	}
	defer conn.Close()

	brWriter := bare.NewWriter(conn)
	brReader := bare.NewReader(conn)

	if err := brWriter.WriteData([]byte(repoPath)); err != nil {
	if err := brWriter.WriteData(stringToBytes(repoPath)); err != nil {
		errorPage500(writer, params, "sending repo path failed: "+err.Error())
		return
	}
	if err := brWriter.WriteUint(2); err != nil {
		errorPage500(writer, params, "sending command failed: "+err.Error())
		return
	}
	if err := brWriter.WriteData([]byte(pathSpec)); err != nil {
	if err := brWriter.WriteData(stringToBytes(pathSpec)); err != nil {
		errorPage500(writer, params, "sending path failed: "+err.Error())
		return
	}

	status, err := brReader.ReadUint()
	if err != nil {
		errorPage500(writer, params, "reading status failed: "+err.Error())
		return
	}

	switch status {
	case 0:
		kind, err := brReader.ReadUint()
		if err != nil {
			errorPage500(writer, params, "reading object kind failed: "+err.Error())
			return
		}

		switch kind {
		case 1:
			// Tree
			if redirectDir(writer, request) {
				return
			}
			count, err := brReader.ReadUint()
			if err != nil {
				errorPage500(writer, params, "reading entry count failed: "+err.Error())
				return
			}

			files := make([]displayTreeEntry, 0, count)
			for range count {
				typeCode, err := brReader.ReadUint()
				if err != nil {
					errorPage500(writer, params, "error reading entry type: "+err.Error())
					return
				}
				mode, err := brReader.ReadUint()
				if err != nil {
					errorPage500(writer, params, "error reading entry mode: "+err.Error())
					return
				}
				size, err := brReader.ReadUint()
				if err != nil {
					errorPage500(writer, params, "error reading entry size: "+err.Error())
					return
				}
				name, err := brReader.ReadData()
				if err != nil {
					errorPage500(writer, params, "error reading entry name: "+err.Error())
					return
				}
				files = append(files, displayTreeEntry{
					Name:      string(name),
					Mode:      fmt.Sprintf("%06o", mode),
					Size:      size,
					IsFile:    typeCode == 2,
					IsSubtree: typeCode == 1,
				})
			}

			params["files"] = files
			params["readme_filename"] = "README.md"
			params["readme"] = template.HTML("<p>README rendering here is WIP again</p>") // TODO

			renderTemplate(writer, "repo_raw_dir", params)

		case 2:
			// Blob
			if redirectNoDir(writer, request) {
				return
			}
			content, err := brReader.ReadData()
			if err != nil && !errors.Is(err, io.EOF) {
				errorPage500(writer, params, "error reading blob content: "+err.Error())
				return
			}
			writer.Header().Set("Content-Type", "application/octet-stream")
			fmt.Fprint(writer, string(content))

		default:
			errorPage500(writer, params, fmt.Sprintf("unknown object kind: %d", kind))
		}

	case 3:
		errorPage500(writer, params, "path not found: "+pathSpec)

	default:
		errorPage500(writer, params, fmt.Sprintf("unknown status code: %d", status))
	}
}
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>

package main

import (
	"errors"
	"fmt"
	"html/template"
	"io"
	"net"
	"net/http"
	"strings"

	"git.sr.ht/~sircmpwn/go-bare"
)

// httpHandleRepoTree provides a friendly, syntax-highlighted view of
// individual files, and provides directory views that link to these files.
//
// TODO: Do not highlight files that are too large.
func httpHandleRepoTree(writer http.ResponseWriter, request *http.Request, params map[string]any) {
	repoName := params["repo_name"].(string)
	groupPath := params["group_path"].([]string)
	rawPathSpec := params["rest"].(string)
	pathSpec := strings.TrimSuffix(rawPathSpec, "/")
	params["path_spec"] = pathSpec

	_, repoPath, _, _, _, _, _ := getRepoInfo(request.Context(), groupPath, repoName, "")

	conn, err := net.Dial("unix", config.Git.Socket)
	if err != nil {
		errorPage500(writer, params, "git2d connection failed: "+err.Error())
		return
	}
	defer conn.Close()

	brWriter := bare.NewWriter(conn)
	brReader := bare.NewReader(conn)

	if err := brWriter.WriteData([]byte(repoPath)); err != nil {
	if err := brWriter.WriteData(stringToBytes(repoPath)); err != nil {
		errorPage500(writer, params, "sending repo path failed: "+err.Error())
		return
	}
	if err := brWriter.WriteUint(2); err != nil {
		errorPage500(writer, params, "sending command failed: "+err.Error())
		return
	}
	if err := brWriter.WriteData([]byte(pathSpec)); err != nil {
	if err := brWriter.WriteData(stringToBytes(pathSpec)); err != nil {
		errorPage500(writer, params, "sending path failed: "+err.Error())
		return
	}

	status, err := brReader.ReadUint()
	if err != nil {
		errorPage500(writer, params, "reading status failed: "+err.Error())
		return
	}

	switch status {
	case 0:
		kind, err := brReader.ReadUint()
		if err != nil {
			errorPage500(writer, params, "reading object kind failed: "+err.Error())
			return
		}

		switch kind {
		case 1:
			// Tree
			count, err := brReader.ReadUint()
			if err != nil {
				errorPage500(writer, params, "reading entry count failed: "+err.Error())
				return
			}
			files := make([]displayTreeEntry, 0, count)
			for range count {
				typeCode, err := brReader.ReadUint()
				if err != nil {
					errorPage500(writer, params, "error reading entry type: "+err.Error())
					return
				}
				mode, err := brReader.ReadUint()
				if err != nil {
					errorPage500(writer, params, "error reading entry mode: "+err.Error())
					return
				}
				size, err := brReader.ReadUint()
				if err != nil {
					errorPage500(writer, params, "error reading entry size: "+err.Error())
					return
				}
				name, err := brReader.ReadData()
				if err != nil {
					errorPage500(writer, params, "error reading entry name: "+err.Error())
					return
				}

				files = append(files, displayTreeEntry{
					Name:      string(name),
					Mode:      fmt.Sprintf("%06o", mode),
					Size:      size,
					IsFile:    typeCode == 2,
					IsSubtree: typeCode == 1,
				})
			}
			params["files"] = files
			params["readme_filename"] = "README.md"
			params["readme"] = template.HTML("<p>README rendering here is WIP again</p>") // TODO
			renderTemplate(writer, "repo_tree_dir", params)

		case 2:
			// Blob
			content, err := brReader.ReadData()
			if err != nil && !errors.Is(err, io.EOF) {
				errorPage500(writer, params, "error reading file content: "+err.Error())
				return
			}
			rendered := renderHighlightedFile(pathSpec, string(content))
			params["file_contents"] = rendered
			renderTemplate(writer, "repo_tree_file", params)

		default:
			errorPage500(writer, params, fmt.Sprintf("unknown kind: %d", kind))
			return
		}

	case 3:
		errorPage500(writer, params, "path not found: "+pathSpec)
		return

	default:
		errorPage500(writer, params, fmt.Sprintf("unknown status code: %d", status))
	}
}
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>

package main

import (
	"bytes"
	"html"
	"html/template"
	"strings"

	"github.com/go-git/go-git/v5/plumbing/object"
	"github.com/microcosm-cc/bluemonday"
	"github.com/niklasfasching/go-org/org"
	"github.com/yuin/goldmark"
	"github.com/yuin/goldmark/extension"
)

var markdownConverter = goldmark.New(goldmark.WithExtensions(extension.GFM))

// escapeHTML just escapes a string and wraps it in [template.HTML].
func escapeHTML(s string) template.HTML {
	return template.HTML(html.EscapeString(s)) //#nosec G203
}

// renderReadmeAtTree looks for README files in the supplied Git tree and
// returns its filename and rendered (and sanitized) HTML.
func renderReadmeAtTree(tree *object.Tree) (string, template.HTML) {
	for _, name := range []string{"README", "README.md", "README.org"} {
		file, err := tree.File(name)
		if err != nil {
			continue
		}
		contents, err := file.Contents()
		if err != nil {
			return "Error fetching README", escapeHTML("Unable to fetch contents of " + name + ": " + err.Error())
		}
		return renderReadme([]byte(contents), name)
		return renderReadme(stringToBytes(contents), name)
	}
	return "", ""
}

// renderReadme renders and sanitizes README content from a byte slice and filename.
func renderReadme(data []byte, filename string) (string, template.HTML) {
	switch strings.ToLower(filename) {
	case "readme":
		return "README", template.HTML("<pre>" + html.EscapeString(string(data)) + "</pre>") //#nosec G203
	case "readme.md":
		var buf bytes.Buffer
		if err := markdownConverter.Convert(data, &buf); err != nil {
			return "Error fetching README", escapeHTML("Unable to render README: " + err.Error())
		}
		return "README.md", template.HTML(bluemonday.UGCPolicy().SanitizeBytes(buf.Bytes())) //#nosec G203
	case "readme.org":
		htmlStr, err := org.New().Parse(strings.NewReader(string(data)), filename).Write(org.NewHTMLWriter())
		if err != nil {
			return "Error fetching README", escapeHTML("Unable to render README: " + err.Error())
		}
		return "README.org", template.HTML(bluemonday.UGCPolicy().Sanitize(htmlStr)) //#nosec G203
	default:
		return filename, template.HTML("<pre>" + html.EscapeString(string(data)) + "</pre>") //#nosec G203
	}
}