From e0635b47c2f30719e1ea026812af85c988632c0e Mon Sep 17 00:00:00 2001 From: Runxi Yu Date: Sat, 5 Apr 2025 21:27:17 +0800 Subject: Move things to internal/ --- ansiec/colors.go | 23 ------- ansiec/reset.go | 3 - ansiec/style.go | 8 --- git2c/client.go | 39 ----------- git2c/cmd1.go | 60 ----------------- git2c/cmd2.go | 89 ------------------------- git2c/git_types.go | 22 ------- git_hooks_handle_linux.go | 4 +- git_hooks_handle_other.go | 4 +- git_plumbing.go | 2 +- http_handle_branches.go | 2 +- http_handle_group_index.go | 2 +- http_handle_repo_commit.go | 2 +- http_handle_repo_index.go | 4 +- http_handle_repo_raw.go | 4 +- http_handle_repo_tree.go | 4 +- http_server.go | 2 +- internal/ansiec/colors.go | 23 +++++++ internal/ansiec/reset.go | 3 + internal/ansiec/style.go | 8 +++ internal/git2c/client.go | 39 +++++++++++ internal/git2c/cmd1.go | 60 +++++++++++++++++ internal/git2c/cmd2.go | 89 +++++++++++++++++++++++++ internal/git2c/git_types.go | 22 +++++++ internal/misc/misc.go | 21 ++++++ internal/misc/unsafe.go | 20 ++++++ internal/misc/url.go | 154 ++++++++++++++++++++++++++++++++++++++++++++ internal/render/chroma.go | 35 ++++++++++ internal/render/escape.go | 11 ++++ internal/render/readme.go | 41 ++++++++++++ lmtp_handle_patch.go | 2 +- lmtp_server.go | 2 +- misc/misc.go | 21 ------ misc/unsafe.go | 20 ------ misc/url.go | 154 -------------------------------------------- remote_url.go | 2 +- render/chroma.go | 35 ---------- render/escape.go | 11 ---- render/readme.go | 41 ------------ resources.go | 2 +- ssh_server.go | 4 +- ssh_utils.go | 4 +- 42 files changed, 549 insertions(+), 549 deletions(-) delete mode 100644 ansiec/colors.go delete mode 100644 ansiec/reset.go delete mode 100644 ansiec/style.go delete mode 100644 git2c/client.go delete mode 100644 git2c/cmd1.go delete mode 100644 git2c/cmd2.go delete mode 100644 git2c/git_types.go create mode 100644 internal/ansiec/colors.go create mode 100644 internal/ansiec/reset.go create mode 100644 internal/ansiec/style.go create mode 100644 internal/git2c/client.go create mode 100644 internal/git2c/cmd1.go create mode 100644 internal/git2c/cmd2.go create mode 100644 internal/git2c/git_types.go create mode 100644 internal/misc/misc.go create mode 100644 internal/misc/unsafe.go create mode 100644 internal/misc/url.go create mode 100644 internal/render/chroma.go create mode 100644 internal/render/escape.go create mode 100644 internal/render/readme.go delete mode 100644 misc/misc.go delete mode 100644 misc/unsafe.go delete mode 100644 misc/url.go delete mode 100644 render/chroma.go delete mode 100644 render/escape.go delete mode 100644 render/readme.go diff --git a/ansiec/colors.go b/ansiec/colors.go deleted file mode 100644 index fa8ea4f..0000000 --- a/ansiec/colors.go +++ /dev/null @@ -1,23 +0,0 @@ -package ansiec - -var ( - Black = "\x1b[30m" - Red = "\x1b[31m" - Green = "\x1b[32m" - Yellow = "\x1b[33m" - Blue = "\x1b[34m" - Magenta = "\x1b[35m" - Cyan = "\x1b[36m" - White = "\x1b[37m" -) - -var ( - BrightBlack = "\x1b[30;1m" - BrightRed = "\x1b[31;1m" - BrightGreen = "\x1b[32;1m" - BrightYellow = "\x1b[33;1m" - BrightBlue = "\x1b[34;1m" - BrightMagenta = "\x1b[35;1m" - BrightCyan = "\x1b[36;1m" - BrightWhite = "\x1b[37;1m" -) diff --git a/ansiec/reset.go b/ansiec/reset.go deleted file mode 100644 index 82a56d5..0000000 --- a/ansiec/reset.go +++ /dev/null @@ -1,3 +0,0 @@ -package ansiec - -var Reset = "\x1b[0m" diff --git a/ansiec/style.go b/ansiec/style.go deleted file mode 100644 index 18050df..0000000 --- a/ansiec/style.go +++ /dev/null @@ -1,8 +0,0 @@ -package ansiec - -var ( - Bold = "\x1b[1m" - Underline = "\x1b[4m" - Reversed = "\x1b[7m" - Italic = "\x1b[3m" -) diff --git a/git2c/client.go b/git2c/client.go deleted file mode 100644 index c4c3ab4..0000000 --- a/git2c/client.go +++ /dev/null @@ -1,39 +0,0 @@ -package git2c - -import ( - "fmt" - "net" - - "git.sr.ht/~sircmpwn/go-bare" -) - -type Client struct { - SocketPath string - conn net.Conn - writer *bare.Writer - reader *bare.Reader -} - -func NewClient(socketPath string) (*Client, error) { - conn, err := net.Dial("unix", socketPath) - if err != nil { - return nil, fmt.Errorf("git2d connection failed: %w", err) - } - - writer := bare.NewWriter(conn) - reader := bare.NewReader(conn) - - return &Client{ - SocketPath: socketPath, - conn: conn, - writer: writer, - reader: reader, - }, nil -} - -func (c *Client) Close() error { - if c.conn != nil { - return c.conn.Close() - } - return nil -} diff --git a/git2c/cmd1.go b/git2c/cmd1.go deleted file mode 100644 index ba59b5a..0000000 --- a/git2c/cmd1.go +++ /dev/null @@ -1,60 +0,0 @@ -package git2c - -import ( - "encoding/hex" - "errors" - "fmt" - "io" -) - -func (c *Client) Cmd1(repoPath string) ([]Commit, *FilenameContents, error) { - if err := c.writer.WriteData([]byte(repoPath)); err != nil { - return nil, nil, fmt.Errorf("sending repo path failed: %w", err) - } - if err := c.writer.WriteUint(1); err != nil { - return nil, nil, fmt.Errorf("sending command failed: %w", err) - } - - status, err := c.reader.ReadUint() - if err != nil { - return nil, nil, fmt.Errorf("reading status failed: %w", err) - } - if status != 0 { - return nil, nil, fmt.Errorf("git2d error: %d", status) - } - - // README - readmeRaw, err := c.reader.ReadData() - if err != nil { - readmeRaw = nil - } - - readmeFilename := "README.md" // TODO - readme := &FilenameContents{Filename: readmeFilename, Content: readmeRaw} - - // Commits - var commits []Commit - for { - id, err := c.reader.ReadData() - if err != nil { - if errors.Is(err, io.EOF) { - break - } - return nil, nil, fmt.Errorf("reading commit ID failed: %w", err) - } - title, _ := c.reader.ReadData() - authorName, _ := c.reader.ReadData() - authorEmail, _ := c.reader.ReadData() - authorDate, _ := c.reader.ReadData() - - commits = append(commits, Commit{ - Hash: hex.EncodeToString(id), - Author: string(authorName), - Email: string(authorEmail), - Date: string(authorDate), - Message: string(title), - }) - } - - return commits, readme, nil -} diff --git a/git2c/cmd2.go b/git2c/cmd2.go deleted file mode 100644 index c688dd2..0000000 --- a/git2c/cmd2.go +++ /dev/null @@ -1,89 +0,0 @@ -package git2c - -import ( - "errors" - "fmt" - "io" -) - -func (c *Client) Cmd2(repoPath, pathSpec string) ([]TreeEntry, string, error) { - if err := c.writer.WriteData([]byte(repoPath)); err != nil { - return nil, "", fmt.Errorf("sending repo path failed: %w", err) - } - if err := c.writer.WriteUint(2); err != nil { - return nil, "", fmt.Errorf("sending command failed: %w", err) - } - if err := c.writer.WriteData([]byte(pathSpec)); err != nil { - return nil, "", fmt.Errorf("sending path failed: %w", err) - } - - status, err := c.reader.ReadUint() - if err != nil { - return nil, "", fmt.Errorf("reading status failed: %w", err) - } - - switch status { - case 0: - kind, err := c.reader.ReadUint() - if err != nil { - return nil, "", fmt.Errorf("reading object kind failed: %w", err) - } - - switch kind { - case 1: - // Tree - count, err := c.reader.ReadUint() - if err != nil { - return nil, "", fmt.Errorf("reading entry count failed: %w", err) - } - - var files []TreeEntry - for range count { - typeCode, err := c.reader.ReadUint() - if err != nil { - return nil, "", fmt.Errorf("error reading entry type: %w", err) - } - mode, err := c.reader.ReadUint() - if err != nil { - return nil, "", fmt.Errorf("error reading entry mode: %w", err) - } - size, err := c.reader.ReadUint() - if err != nil { - return nil, "", fmt.Errorf("error reading entry size: %w", err) - } - name, err := c.reader.ReadData() - if err != nil { - return nil, "", fmt.Errorf("error reading entry name: %w", err) - } - - files = append(files, TreeEntry{ - Name: string(name), - Mode: fmt.Sprintf("%06o", mode), - Size: size, - IsFile: typeCode == 2, - IsSubtree: typeCode == 1, - }) - } - - return files, "", nil - - case 2: - // Blob - content, err := c.reader.ReadData() - if err != nil && !errors.Is(err, io.EOF) { - return nil, "", fmt.Errorf("error reading file content: %w", err) - } - - return nil, string(content), nil - - default: - return nil, "", fmt.Errorf("unknown kind: %d", kind) - } - - case 3: - return nil, "", fmt.Errorf("path not found: %s", pathSpec) - - default: - return nil, "", fmt.Errorf("unknown status code: %d", status) - } -} diff --git a/git2c/git_types.go b/git2c/git_types.go deleted file mode 100644 index da90db6..0000000 --- a/git2c/git_types.go +++ /dev/null @@ -1,22 +0,0 @@ -package git2c - -type Commit struct { - Hash string - Author string - Email string - Date string - Message string -} - -type FilenameContents struct { - Filename string - Content []byte -} - -type TreeEntry struct { - Name string - Mode string - Size uint64 - IsFile bool - IsSubtree bool -} diff --git a/git_hooks_handle_linux.go b/git_hooks_handle_linux.go index 1101302..3d8c011 100644 --- a/git_hooks_handle_linux.go +++ b/git_hooks_handle_linux.go @@ -23,8 +23,8 @@ import ( "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" "github.com/jackc/pgx/v5" - "go.lindenii.runxiyu.org/forge/ansiec" - "go.lindenii.runxiyu.org/forge/misc" + "go.lindenii.runxiyu.org/forge/internal/ansiec" + "go.lindenii.runxiyu.org/forge/internal/misc" ) var ( diff --git a/git_hooks_handle_other.go b/git_hooks_handle_other.go index 4a4328f..f125ecb 100644 --- a/git_hooks_handle_other.go +++ b/git_hooks_handle_other.go @@ -21,8 +21,8 @@ import ( "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" "github.com/jackc/pgx/v5" - "go.lindenii.runxiyu.org/forge/ansiec" - "go.lindenii.runxiyu.org/forge/misc" + "go.lindenii.runxiyu.org/forge/internal/ansiec" + "go.lindenii.runxiyu.org/forge/internal/misc" ) var errGetFD = errors.New("unable to get file descriptor") diff --git a/git_plumbing.go b/git_plumbing.go index 440de7c..8c73a93 100644 --- a/git_plumbing.go +++ b/git_plumbing.go @@ -14,7 +14,7 @@ import ( "sort" "strings" - "go.lindenii.runxiyu.org/forge/misc" + "go.lindenii.runxiyu.org/forge/internal/misc" ) func writeTree(ctx context.Context, repoPath string, entries []treeEntry) (string, error) { diff --git a/http_handle_branches.go b/http_handle_branches.go index 96c4ac7..659287f 100644 --- a/http_handle_branches.go +++ b/http_handle_branches.go @@ -10,7 +10,7 @@ import ( "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/storer" - "go.lindenii.runxiyu.org/forge/misc" + "go.lindenii.runxiyu.org/forge/internal/misc" ) // httpHandleRepoBranches provides the branches page in repos. diff --git a/http_handle_group_index.go b/http_handle_group_index.go index e0e460d..3f6ed68 100644 --- a/http_handle_group_index.go +++ b/http_handle_group_index.go @@ -11,7 +11,7 @@ import ( "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" - "go.lindenii.runxiyu.org/forge/misc" + "go.lindenii.runxiyu.org/forge/internal/misc" ) // httpHandleGroupIndex provides index pages for groups, which includes a list diff --git a/http_handle_repo_commit.go b/http_handle_repo_commit.go index 88ade4b..1042aff 100644 --- a/http_handle_repo_commit.go +++ b/http_handle_repo_commit.go @@ -13,7 +13,7 @@ import ( "github.com/go-git/go-git/v5/plumbing/filemode" "github.com/go-git/go-git/v5/plumbing/format/diff" "github.com/go-git/go-git/v5/plumbing/object" - "go.lindenii.runxiyu.org/forge/misc" + "go.lindenii.runxiyu.org/forge/internal/misc" ) // usableFilePatch is a [diff.FilePatch] that is structured in a way more diff --git a/http_handle_repo_index.go b/http_handle_repo_index.go index edba57b..40fd9b0 100644 --- a/http_handle_repo_index.go +++ b/http_handle_repo_index.go @@ -6,8 +6,8 @@ package forge import ( "net/http" - "go.lindenii.runxiyu.org/forge/git2c" - "go.lindenii.runxiyu.org/forge/render" + "go.lindenii.runxiyu.org/forge/internal/git2c" + "go.lindenii.runxiyu.org/forge/internal/render" ) type commitDisplay struct { diff --git a/http_handle_repo_raw.go b/http_handle_repo_raw.go index 7e19e02..b714397 100644 --- a/http_handle_repo_raw.go +++ b/http_handle_repo_raw.go @@ -9,8 +9,8 @@ import ( "net/http" "strings" - "go.lindenii.runxiyu.org/forge/git2c" - "go.lindenii.runxiyu.org/forge/misc" + "go.lindenii.runxiyu.org/forge/internal/git2c" + "go.lindenii.runxiyu.org/forge/internal/misc" ) // httpHandleRepoRaw serves raw files, or directory listings that point to raw diff --git a/http_handle_repo_tree.go b/http_handle_repo_tree.go index e8e5ff8..00c6821 100644 --- a/http_handle_repo_tree.go +++ b/http_handle_repo_tree.go @@ -8,8 +8,8 @@ import ( "net/http" "strings" - "go.lindenii.runxiyu.org/forge/git2c" - "go.lindenii.runxiyu.org/forge/render" + "go.lindenii.runxiyu.org/forge/internal/git2c" + "go.lindenii.runxiyu.org/forge/internal/render" ) // httpHandleRepoTree provides a friendly, syntax-highlighted view of diff --git a/http_server.go b/http_server.go index fdb55b4..cad3a3d 100644 --- a/http_server.go +++ b/http_server.go @@ -12,7 +12,7 @@ import ( "strings" "github.com/jackc/pgx/v5" - "go.lindenii.runxiyu.org/forge/misc" + "go.lindenii.runxiyu.org/forge/internal/misc" ) // ServeHTTP handles all incoming HTTP requests and routes them to the correct diff --git a/internal/ansiec/colors.go b/internal/ansiec/colors.go new file mode 100644 index 0000000..fa8ea4f --- /dev/null +++ b/internal/ansiec/colors.go @@ -0,0 +1,23 @@ +package ansiec + +var ( + Black = "\x1b[30m" + Red = "\x1b[31m" + Green = "\x1b[32m" + Yellow = "\x1b[33m" + Blue = "\x1b[34m" + Magenta = "\x1b[35m" + Cyan = "\x1b[36m" + White = "\x1b[37m" +) + +var ( + BrightBlack = "\x1b[30;1m" + BrightRed = "\x1b[31;1m" + BrightGreen = "\x1b[32;1m" + BrightYellow = "\x1b[33;1m" + BrightBlue = "\x1b[34;1m" + BrightMagenta = "\x1b[35;1m" + BrightCyan = "\x1b[36;1m" + BrightWhite = "\x1b[37;1m" +) diff --git a/internal/ansiec/reset.go b/internal/ansiec/reset.go new file mode 100644 index 0000000..82a56d5 --- /dev/null +++ b/internal/ansiec/reset.go @@ -0,0 +1,3 @@ +package ansiec + +var Reset = "\x1b[0m" diff --git a/internal/ansiec/style.go b/internal/ansiec/style.go new file mode 100644 index 0000000..18050df --- /dev/null +++ b/internal/ansiec/style.go @@ -0,0 +1,8 @@ +package ansiec + +var ( + Bold = "\x1b[1m" + Underline = "\x1b[4m" + Reversed = "\x1b[7m" + Italic = "\x1b[3m" +) diff --git a/internal/git2c/client.go b/internal/git2c/client.go new file mode 100644 index 0000000..c4c3ab4 --- /dev/null +++ b/internal/git2c/client.go @@ -0,0 +1,39 @@ +package git2c + +import ( + "fmt" + "net" + + "git.sr.ht/~sircmpwn/go-bare" +) + +type Client struct { + SocketPath string + conn net.Conn + writer *bare.Writer + reader *bare.Reader +} + +func NewClient(socketPath string) (*Client, error) { + conn, err := net.Dial("unix", socketPath) + if err != nil { + return nil, fmt.Errorf("git2d connection failed: %w", err) + } + + writer := bare.NewWriter(conn) + reader := bare.NewReader(conn) + + return &Client{ + SocketPath: socketPath, + conn: conn, + writer: writer, + reader: reader, + }, nil +} + +func (c *Client) Close() error { + if c.conn != nil { + return c.conn.Close() + } + return nil +} diff --git a/internal/git2c/cmd1.go b/internal/git2c/cmd1.go new file mode 100644 index 0000000..ba59b5a --- /dev/null +++ b/internal/git2c/cmd1.go @@ -0,0 +1,60 @@ +package git2c + +import ( + "encoding/hex" + "errors" + "fmt" + "io" +) + +func (c *Client) Cmd1(repoPath string) ([]Commit, *FilenameContents, error) { + if err := c.writer.WriteData([]byte(repoPath)); err != nil { + return nil, nil, fmt.Errorf("sending repo path failed: %w", err) + } + if err := c.writer.WriteUint(1); err != nil { + return nil, nil, fmt.Errorf("sending command failed: %w", err) + } + + status, err := c.reader.ReadUint() + if err != nil { + return nil, nil, fmt.Errorf("reading status failed: %w", err) + } + if status != 0 { + return nil, nil, fmt.Errorf("git2d error: %d", status) + } + + // README + readmeRaw, err := c.reader.ReadData() + if err != nil { + readmeRaw = nil + } + + readmeFilename := "README.md" // TODO + readme := &FilenameContents{Filename: readmeFilename, Content: readmeRaw} + + // Commits + var commits []Commit + for { + id, err := c.reader.ReadData() + if err != nil { + if errors.Is(err, io.EOF) { + break + } + return nil, nil, fmt.Errorf("reading commit ID failed: %w", err) + } + title, _ := c.reader.ReadData() + authorName, _ := c.reader.ReadData() + authorEmail, _ := c.reader.ReadData() + authorDate, _ := c.reader.ReadData() + + commits = append(commits, Commit{ + Hash: hex.EncodeToString(id), + Author: string(authorName), + Email: string(authorEmail), + Date: string(authorDate), + Message: string(title), + }) + } + + return commits, readme, nil +} diff --git a/internal/git2c/cmd2.go b/internal/git2c/cmd2.go new file mode 100644 index 0000000..c688dd2 --- /dev/null +++ b/internal/git2c/cmd2.go @@ -0,0 +1,89 @@ +package git2c + +import ( + "errors" + "fmt" + "io" +) + +func (c *Client) Cmd2(repoPath, pathSpec string) ([]TreeEntry, string, error) { + if err := c.writer.WriteData([]byte(repoPath)); err != nil { + return nil, "", fmt.Errorf("sending repo path failed: %w", err) + } + if err := c.writer.WriteUint(2); err != nil { + return nil, "", fmt.Errorf("sending command failed: %w", err) + } + if err := c.writer.WriteData([]byte(pathSpec)); err != nil { + return nil, "", fmt.Errorf("sending path failed: %w", err) + } + + status, err := c.reader.ReadUint() + if err != nil { + return nil, "", fmt.Errorf("reading status failed: %w", err) + } + + switch status { + case 0: + kind, err := c.reader.ReadUint() + if err != nil { + return nil, "", fmt.Errorf("reading object kind failed: %w", err) + } + + switch kind { + case 1: + // Tree + count, err := c.reader.ReadUint() + if err != nil { + return nil, "", fmt.Errorf("reading entry count failed: %w", err) + } + + var files []TreeEntry + for range count { + typeCode, err := c.reader.ReadUint() + if err != nil { + return nil, "", fmt.Errorf("error reading entry type: %w", err) + } + mode, err := c.reader.ReadUint() + if err != nil { + return nil, "", fmt.Errorf("error reading entry mode: %w", err) + } + size, err := c.reader.ReadUint() + if err != nil { + return nil, "", fmt.Errorf("error reading entry size: %w", err) + } + name, err := c.reader.ReadData() + if err != nil { + return nil, "", fmt.Errorf("error reading entry name: %w", err) + } + + files = append(files, TreeEntry{ + Name: string(name), + Mode: fmt.Sprintf("%06o", mode), + Size: size, + IsFile: typeCode == 2, + IsSubtree: typeCode == 1, + }) + } + + return files, "", nil + + case 2: + // Blob + content, err := c.reader.ReadData() + if err != nil && !errors.Is(err, io.EOF) { + return nil, "", fmt.Errorf("error reading file content: %w", err) + } + + return nil, string(content), nil + + default: + return nil, "", fmt.Errorf("unknown kind: %d", kind) + } + + case 3: + return nil, "", fmt.Errorf("path not found: %s", pathSpec) + + default: + return nil, "", fmt.Errorf("unknown status code: %d", status) + } +} diff --git a/internal/git2c/git_types.go b/internal/git2c/git_types.go new file mode 100644 index 0000000..da90db6 --- /dev/null +++ b/internal/git2c/git_types.go @@ -0,0 +1,22 @@ +package git2c + +type Commit struct { + Hash string + Author string + Email string + Date string + Message string +} + +type FilenameContents struct { + Filename string + Content []byte +} + +type TreeEntry struct { + Name string + Mode string + Size uint64 + IsFile bool + IsSubtree bool +} diff --git a/internal/misc/misc.go b/internal/misc/misc.go new file mode 100644 index 0000000..ee0fd7a --- /dev/null +++ b/internal/misc/misc.go @@ -0,0 +1,21 @@ +package misc + +import "strings" + +func FirstOrPanic[T any](v T, err error) T { + if err != nil { + panic(err) + } + return v +} + +// sliceContainsNewlines returns true if and only if the given slice contains +// one or more strings that contains newlines. +func SliceContainsNewlines(s []string) bool { + for _, v := range s { + if strings.Contains(v, "\n") { + return true + } + } + return false +} diff --git a/internal/misc/unsafe.go b/internal/misc/unsafe.go new file mode 100644 index 0000000..6c2192f --- /dev/null +++ b/internal/misc/unsafe.go @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu + +package misc + +import "unsafe" + +// StringToBytes converts a string to a byte slice without copying the string. +// Memory is borrowed from the string. +// The resulting byte slice must not be modified in any form. +func StringToBytes(s string) (bytes []byte) { + return unsafe.Slice(unsafe.StringData(s), len(s)) +} + +// BytesToString converts a byte slice to a string without copying the bytes. +// Memory is borrowed from the byte slice. +// The source byte slice must not be modified. +func BytesToString(b []byte) string { + return unsafe.String(unsafe.SliceData(b), len(b)) +} diff --git a/internal/misc/url.go b/internal/misc/url.go new file mode 100644 index 0000000..b77d8ce --- /dev/null +++ b/internal/misc/url.go @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu + +package misc + +import ( + "errors" + "net/http" + "net/url" + "strings" +) + +var ( + ErrDupRefSpec = errors.New("duplicate ref spec") + ErrNoRefSpec = errors.New("no ref spec") +) + +// getParamRefTypeName looks at the query parameters in an HTTP request and +// returns its ref name and type, if any. +func GetParamRefTypeName(request *http.Request) (retRefType, retRefName string, err error) { + rawQuery := request.URL.RawQuery + queryValues, err := url.ParseQuery(rawQuery) + if err != nil { + return + } + done := false + for _, refType := range []string{"commit", "branch", "tag"} { + refName, ok := queryValues[refType] + if ok { + if done { + err = ErrDupRefSpec + return + } + done = true + if len(refName) != 1 { + err = ErrDupRefSpec + return + } + retRefName = refName[0] + retRefType = refType + } + } + if !done { + err = ErrNoRefSpec + } + return +} + +// ParseReqURI parses an HTTP request URL, and returns a slice of path segments +// and the query parameters. It handles %2F correctly. +func ParseReqURI(requestURI string) (segments []string, params url.Values, err error) { + path, paramsStr, _ := strings.Cut(requestURI, "?") + + segments, err = PathToSegments(path) + if err != nil { + return + } + + params, err = url.ParseQuery(paramsStr) + return +} + +func PathToSegments(path string) (segments []string, err error) { + segments = strings.Split(strings.TrimPrefix(path, "/"), "/") + + for i, segment := range segments { + segments[i], err = url.PathUnescape(segment) + if err != nil { + return + } + } + + return +} + +// RedirectDir returns true and redirects the user to a version of the URL with +// a trailing slash, if and only if the request URL does not already have a +// trailing slash. +func RedirectDir(writer http.ResponseWriter, request *http.Request) bool { + requestURI := request.RequestURI + + pathEnd := strings.IndexAny(requestURI, "?#") + var path, rest string + if pathEnd == -1 { + path = requestURI + } else { + path = requestURI[:pathEnd] + rest = requestURI[pathEnd:] + } + + if !strings.HasSuffix(path, "/") { + http.Redirect(writer, request, path+"/"+rest, http.StatusSeeOther) + return true + } + return false +} + +// RedirectNoDir returns true and redirects the user to a version of the URL +// without a trailing slash, if and only if the request URL has a trailing +// slash. +func RedirectNoDir(writer http.ResponseWriter, request *http.Request) bool { + requestURI := request.RequestURI + + pathEnd := strings.IndexAny(requestURI, "?#") + var path, rest string + if pathEnd == -1 { + path = requestURI + } else { + path = requestURI[:pathEnd] + rest = requestURI[pathEnd:] + } + + if strings.HasSuffix(path, "/") { + http.Redirect(writer, request, strings.TrimSuffix(path, "/")+rest, http.StatusSeeOther) + return true + } + return false +} + +// RedirectUnconditionally unconditionally redirects the user back to the +// current page while preserving query parameters. +func RedirectUnconditionally(writer http.ResponseWriter, request *http.Request) { + requestURI := request.RequestURI + + pathEnd := strings.IndexAny(requestURI, "?#") + var path, rest string + if pathEnd == -1 { + path = requestURI + } else { + path = requestURI[:pathEnd] + rest = requestURI[pathEnd:] + } + + http.Redirect(writer, request, path+rest, http.StatusSeeOther) +} + +// SegmentsToURL joins URL segments to the path component of a URL. +// Each segment is escaped properly first. +func SegmentsToURL(segments []string) string { + for i, segment := range segments { + segments[i] = url.PathEscape(segment) + } + return strings.Join(segments, "/") +} + +// AnyContain returns true if and only if ss contains a string that contains c. +func AnyContain(ss []string, c string) bool { + for _, s := range ss { + if strings.Contains(s, c) { + return true + } + } + return false +} diff --git a/internal/render/chroma.go b/internal/render/chroma.go new file mode 100644 index 0000000..c7d64ec --- /dev/null +++ b/internal/render/chroma.go @@ -0,0 +1,35 @@ +package render + +import ( + "bytes" + "html/template" + + chromaHTML "github.com/alecthomas/chroma/v2/formatters/html" + chromaLexers "github.com/alecthomas/chroma/v2/lexers" + chromaStyles "github.com/alecthomas/chroma/v2/styles" +) + +func Highlight(filename, content string) template.HTML { + lexer := chromaLexers.Match(filename) + if lexer == nil { + lexer = chromaLexers.Fallback + } + + iterator, err := lexer.Tokenise(nil, content) + if err != nil { + return template.HTML("
Error tokenizing file: " + err.Error() + "
") //#nosec G203` + } + + var buf bytes.Buffer + style := chromaStyles.Get("autumn") + formatter := chromaHTML.New( + chromaHTML.WithClasses(true), + chromaHTML.TabWidth(8), + ) + + if err := formatter.Format(&buf, style, iterator); err != nil { + return template.HTML("
Error formatting file: " + err.Error() + "
") //#nosec G203 + } + + return template.HTML(buf.Bytes()) //#nosec G203 +} diff --git a/internal/render/escape.go b/internal/render/escape.go new file mode 100644 index 0000000..44c56f3 --- /dev/null +++ b/internal/render/escape.go @@ -0,0 +1,11 @@ +package render + +import ( + "html" + "html/template" +) + +// 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 +} diff --git a/internal/render/readme.go b/internal/render/readme.go new file mode 100644 index 0000000..aecd2f7 --- /dev/null +++ b/internal/render/readme.go @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu + +package render + +import ( + "bytes" + "html" + "html/template" + "strings" + + "github.com/microcosm-cc/bluemonday" + "github.com/niklasfasching/go-org/org" + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/extension" + "go.lindenii.runxiyu.org/forge/internal/misc" +) + +var markdownConverter = goldmark.New(goldmark.WithExtensions(extension.GFM)) + +// renderReadme renders and sanitizes README content from a byte slice and filename. +func Readme(data []byte, filename string) (string, template.HTML) { + switch strings.ToLower(filename) { + case "readme": + return "README", template.HTML("
" + html.EscapeString(misc.BytesToString(data)) + "
") //#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(misc.BytesToString(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("
" + html.EscapeString(misc.BytesToString(data)) + "
") //#nosec G203 + } +} diff --git a/lmtp_handle_patch.go b/lmtp_handle_patch.go index bf1b94c..6841444 100644 --- a/lmtp_handle_patch.go +++ b/lmtp_handle_patch.go @@ -16,7 +16,7 @@ import ( "github.com/bluekeyes/go-gitdiff/gitdiff" "github.com/go-git/go-git/v5" - "go.lindenii.runxiyu.org/forge/misc" + "go.lindenii.runxiyu.org/forge/internal/misc" ) func (s *Server) lmtpHandlePatch(session *lmtpSession, groupPath []string, repoName string, mbox io.Reader) (err error) { diff --git a/lmtp_server.go b/lmtp_server.go index 863a5c0..ae912c5 100644 --- a/lmtp_server.go +++ b/lmtp_server.go @@ -17,7 +17,7 @@ import ( "github.com/emersion/go-message" "github.com/emersion/go-smtp" - "go.lindenii.runxiyu.org/forge/misc" + "go.lindenii.runxiyu.org/forge/internal/misc" ) type lmtpHandler struct{} diff --git a/misc/misc.go b/misc/misc.go deleted file mode 100644 index ee0fd7a..0000000 --- a/misc/misc.go +++ /dev/null @@ -1,21 +0,0 @@ -package misc - -import "strings" - -func FirstOrPanic[T any](v T, err error) T { - if err != nil { - panic(err) - } - return v -} - -// sliceContainsNewlines returns true if and only if the given slice contains -// one or more strings that contains newlines. -func SliceContainsNewlines(s []string) bool { - for _, v := range s { - if strings.Contains(v, "\n") { - return true - } - } - return false -} diff --git a/misc/unsafe.go b/misc/unsafe.go deleted file mode 100644 index 6c2192f..0000000 --- a/misc/unsafe.go +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu - -package misc - -import "unsafe" - -// StringToBytes converts a string to a byte slice without copying the string. -// Memory is borrowed from the string. -// The resulting byte slice must not be modified in any form. -func StringToBytes(s string) (bytes []byte) { - return unsafe.Slice(unsafe.StringData(s), len(s)) -} - -// BytesToString converts a byte slice to a string without copying the bytes. -// Memory is borrowed from the byte slice. -// The source byte slice must not be modified. -func BytesToString(b []byte) string { - return unsafe.String(unsafe.SliceData(b), len(b)) -} diff --git a/misc/url.go b/misc/url.go deleted file mode 100644 index b77d8ce..0000000 --- a/misc/url.go +++ /dev/null @@ -1,154 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu - -package misc - -import ( - "errors" - "net/http" - "net/url" - "strings" -) - -var ( - ErrDupRefSpec = errors.New("duplicate ref spec") - ErrNoRefSpec = errors.New("no ref spec") -) - -// getParamRefTypeName looks at the query parameters in an HTTP request and -// returns its ref name and type, if any. -func GetParamRefTypeName(request *http.Request) (retRefType, retRefName string, err error) { - rawQuery := request.URL.RawQuery - queryValues, err := url.ParseQuery(rawQuery) - if err != nil { - return - } - done := false - for _, refType := range []string{"commit", "branch", "tag"} { - refName, ok := queryValues[refType] - if ok { - if done { - err = ErrDupRefSpec - return - } - done = true - if len(refName) != 1 { - err = ErrDupRefSpec - return - } - retRefName = refName[0] - retRefType = refType - } - } - if !done { - err = ErrNoRefSpec - } - return -} - -// ParseReqURI parses an HTTP request URL, and returns a slice of path segments -// and the query parameters. It handles %2F correctly. -func ParseReqURI(requestURI string) (segments []string, params url.Values, err error) { - path, paramsStr, _ := strings.Cut(requestURI, "?") - - segments, err = PathToSegments(path) - if err != nil { - return - } - - params, err = url.ParseQuery(paramsStr) - return -} - -func PathToSegments(path string) (segments []string, err error) { - segments = strings.Split(strings.TrimPrefix(path, "/"), "/") - - for i, segment := range segments { - segments[i], err = url.PathUnescape(segment) - if err != nil { - return - } - } - - return -} - -// RedirectDir returns true and redirects the user to a version of the URL with -// a trailing slash, if and only if the request URL does not already have a -// trailing slash. -func RedirectDir(writer http.ResponseWriter, request *http.Request) bool { - requestURI := request.RequestURI - - pathEnd := strings.IndexAny(requestURI, "?#") - var path, rest string - if pathEnd == -1 { - path = requestURI - } else { - path = requestURI[:pathEnd] - rest = requestURI[pathEnd:] - } - - if !strings.HasSuffix(path, "/") { - http.Redirect(writer, request, path+"/"+rest, http.StatusSeeOther) - return true - } - return false -} - -// RedirectNoDir returns true and redirects the user to a version of the URL -// without a trailing slash, if and only if the request URL has a trailing -// slash. -func RedirectNoDir(writer http.ResponseWriter, request *http.Request) bool { - requestURI := request.RequestURI - - pathEnd := strings.IndexAny(requestURI, "?#") - var path, rest string - if pathEnd == -1 { - path = requestURI - } else { - path = requestURI[:pathEnd] - rest = requestURI[pathEnd:] - } - - if strings.HasSuffix(path, "/") { - http.Redirect(writer, request, strings.TrimSuffix(path, "/")+rest, http.StatusSeeOther) - return true - } - return false -} - -// RedirectUnconditionally unconditionally redirects the user back to the -// current page while preserving query parameters. -func RedirectUnconditionally(writer http.ResponseWriter, request *http.Request) { - requestURI := request.RequestURI - - pathEnd := strings.IndexAny(requestURI, "?#") - var path, rest string - if pathEnd == -1 { - path = requestURI - } else { - path = requestURI[:pathEnd] - rest = requestURI[pathEnd:] - } - - http.Redirect(writer, request, path+rest, http.StatusSeeOther) -} - -// SegmentsToURL joins URL segments to the path component of a URL. -// Each segment is escaped properly first. -func SegmentsToURL(segments []string) string { - for i, segment := range segments { - segments[i] = url.PathEscape(segment) - } - return strings.Join(segments, "/") -} - -// AnyContain returns true if and only if ss contains a string that contains c. -func AnyContain(ss []string, c string) bool { - for _, s := range ss { - if strings.Contains(s, c) { - return true - } - } - return false -} diff --git a/remote_url.go b/remote_url.go index 453ddeb..8fc5528 100644 --- a/remote_url.go +++ b/remote_url.go @@ -7,7 +7,7 @@ import ( "net/url" "strings" - "go.lindenii.runxiyu.org/forge/misc" + "go.lindenii.runxiyu.org/forge/internal/misc" ) // We don't use path.Join because it collapses multiple slashes into one. diff --git a/render/chroma.go b/render/chroma.go deleted file mode 100644 index c7d64ec..0000000 --- a/render/chroma.go +++ /dev/null @@ -1,35 +0,0 @@ -package render - -import ( - "bytes" - "html/template" - - chromaHTML "github.com/alecthomas/chroma/v2/formatters/html" - chromaLexers "github.com/alecthomas/chroma/v2/lexers" - chromaStyles "github.com/alecthomas/chroma/v2/styles" -) - -func Highlight(filename, content string) template.HTML { - lexer := chromaLexers.Match(filename) - if lexer == nil { - lexer = chromaLexers.Fallback - } - - iterator, err := lexer.Tokenise(nil, content) - if err != nil { - return template.HTML("
Error tokenizing file: " + err.Error() + "
") //#nosec G203` - } - - var buf bytes.Buffer - style := chromaStyles.Get("autumn") - formatter := chromaHTML.New( - chromaHTML.WithClasses(true), - chromaHTML.TabWidth(8), - ) - - if err := formatter.Format(&buf, style, iterator); err != nil { - return template.HTML("
Error formatting file: " + err.Error() + "
") //#nosec G203 - } - - return template.HTML(buf.Bytes()) //#nosec G203 -} diff --git a/render/escape.go b/render/escape.go deleted file mode 100644 index 44c56f3..0000000 --- a/render/escape.go +++ /dev/null @@ -1,11 +0,0 @@ -package render - -import ( - "html" - "html/template" -) - -// 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 -} diff --git a/render/readme.go b/render/readme.go deleted file mode 100644 index 1a153fb..0000000 --- a/render/readme.go +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu - -package render - -import ( - "bytes" - "html" - "html/template" - "strings" - - "github.com/microcosm-cc/bluemonday" - "github.com/niklasfasching/go-org/org" - "github.com/yuin/goldmark" - "github.com/yuin/goldmark/extension" - "go.lindenii.runxiyu.org/forge/misc" -) - -var markdownConverter = goldmark.New(goldmark.WithExtensions(extension.GFM)) - -// renderReadme renders and sanitizes README content from a byte slice and filename. -func Readme(data []byte, filename string) (string, template.HTML) { - switch strings.ToLower(filename) { - case "readme": - return "README", template.HTML("
" + html.EscapeString(misc.BytesToString(data)) + "
") //#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(misc.BytesToString(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("
" + html.EscapeString(misc.BytesToString(data)) + "
") //#nosec G203 - } -} diff --git a/resources.go b/resources.go index ffe1008..0f2e1a9 100644 --- a/resources.go +++ b/resources.go @@ -10,7 +10,7 @@ import ( "github.com/tdewolff/minify/v2" "github.com/tdewolff/minify/v2/html" - "go.lindenii.runxiyu.org/forge/misc" + "go.lindenii.runxiyu.org/forge/internal/misc" ) //go:embed LICENSE source.tar.gz diff --git a/ssh_server.go b/ssh_server.go index ed303b9..07a1f34 100644 --- a/ssh_server.go +++ b/ssh_server.go @@ -11,8 +11,8 @@ import ( "strings" gliderSSH "github.com/gliderlabs/ssh" - "go.lindenii.runxiyu.org/forge/ansiec" - "go.lindenii.runxiyu.org/forge/misc" + "go.lindenii.runxiyu.org/forge/internal/ansiec" + "go.lindenii.runxiyu.org/forge/internal/misc" goSSH "golang.org/x/crypto/ssh" ) diff --git a/ssh_utils.go b/ssh_utils.go index 8f04209..7aeedff 100644 --- a/ssh_utils.go +++ b/ssh_utils.go @@ -10,8 +10,8 @@ import ( "io" "net/url" - "go.lindenii.runxiyu.org/forge/ansiec" - "go.lindenii.runxiyu.org/forge/misc" + "go.lindenii.runxiyu.org/forge/internal/ansiec" + "go.lindenii.runxiyu.org/forge/internal/misc" ) var errIllegalSSHRepoPath = errors.New("illegal SSH repo path") -- cgit v1.2.3