Parent directory

content.go

2770 bytes
 1package pages
 2
 3import (
 4	"bytes"
 5	"errors"
 6	"fmt"
 7	"path"
 8	"strings"
 9
10	"github.com/charmbracelet/soft-serve/git"
11)
12
13const (
14	// TextLimit is the maximum source size rendered in a page.
15	TextLimit int64 = 1 << 20
16	// RawLimit is the maximum source size served by the raw endpoint.
17	RawLimit int64 = 16 << 20
18)
19
20var (
21	// ErrContentTooLarge means the entry was rejected before loading its blob.
22	ErrContentTooLarge = errors.New("content exceeds page size limit")
23	// ErrReadmeNotFound means no regular README blob was found.
24	ErrReadmeNotFound = errors.New("readme not found")
25)
26
27// BlobContent is a bounded blob and its binary classification.
28type BlobContent struct {
29	Bytes  []byte
30	Size   int64
31	Binary bool
32}
33
34// LoadBlob checks an entry's Git-reported size before loading its bytes.
35func LoadBlob(entry *git.TreeEntry, limit int64) (BlobContent, error) {
36	if entry == nil || (!entry.IsBlob() && !entry.IsExec()) {
37		return BlobContent{}, fmt.Errorf("entry is not a regular blob")
38	}
39	size := entry.Size()
40	if size > limit {
41		return BlobContent{}, ErrContentTooLarge
42	}
43	contents, err := entry.Contents()
44	if err != nil {
45		return BlobContent{}, err
46	}
47	// A Git object cannot change after its ID has been read, but keep the limit
48	// invariant if a Git implementation reports an unexpected size.
49	if int64(len(contents)) > limit {
50		return BlobContent{}, ErrContentTooLarge
51	}
52	return BlobContent{Bytes: contents, Size: int64(len(contents)), Binary: IsBinary(contents)}, nil
53}
54
55// IsBinary detects NUL bytes in the first Git-style sniff window of bounded data.
56func IsBinary(contents []byte) bool {
57	const sniffLength = 8000
58	if len(contents) > sniffLength {
59		contents = contents[:sniffLength]
60	}
61	return bytes.IndexByte(contents, 0) >= 0
62}
63
64// Readme contains the bounded README data and its POSIX path.
65type Readme struct {
66	Path string
67	BlobContent
68}
69
70// LoadReadme searches root, .github, then docs for a regular README blob.
71func LoadReadme(repo *git.Repository, ref *git.Reference) (*Readme, error) {
72	for _, directory := range []string{"", ".github", "docs"} {
73		tree, err := repo.TreePath(ref, directory)
74		if err != nil {
75			continue
76		}
77		entries, err := tree.Entries()
78		if err != nil {
79			continue
80		}
81		for _, entry := range entries {
82			// IsBlob intentionally excludes executable blobs and symlinks. README
83			// symlinks must not cause a second path lookup.
84			if !entry.IsBlob() || !isReadmeName(entry.Name()) {
85				continue
86			}
87			content, err := LoadBlob(entry, TextLimit)
88			if err != nil {
89				return nil, err
90			}
91			return &Readme{Path: path.Join(directory, entry.Name()), BlobContent: content}, nil
92		}
93	}
94	return nil, ErrReadmeNotFound
95}
96
97func isReadmeName(name string) bool {
98	return strings.HasPrefix(strings.ToLower(name), "readme")
99}