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}