2d862ecc14da09a6a21fa9b7c04f59e0ebe2111f
- Author
- TheEdgeOfRage <git@theedgeofrage.com>
- Committer
- TheEdgeOfRage <git@theedgeofrage.com>
- Date
Message
Diff
This diff is truncated to protect this page.
1diff --git a/go.mod b/go.mod
2index d20b2543ed91e024530fc9b3f8f473fcd911d76f..4cfe73f1061680619aa0be16e852959ab77a334d 100644
3--- a/go.mod
4+++ b/go.mod
5@@ -32,6 +32,7 @@ require (
6 github.com/lib/pq v1.12.3
7 github.com/lrstanley/bubblezone/v2 v2.0.0
8 github.com/matryer/is v1.4.1
9+ github.com/microcosm-cc/bluemonday v1.0.27
10 github.com/muesli/mango-cobra v1.3.0
11 github.com/muesli/reflow v0.3.0
12 github.com/muesli/roff v0.1.0
13@@ -40,6 +41,7 @@ require (
14 github.com/rogpeppe/go-internal v1.16.0
15 github.com/sergi/go-diff v1.4.0
16 github.com/spf13/cobra v1.10.2
17+ github.com/yuin/goldmark v1.8.5
18 go.uber.org/automaxprocs v1.6.0
19 golang.org/x/crypto v0.54.0
20 golang.org/x/sync v0.22.0
21@@ -74,7 +76,6 @@ require (
22 github.com/mattn/go-isatty v0.0.24 // indirect
23 github.com/mattn/go-runewidth v0.0.27 // indirect
24 github.com/mcuadros/go-version v0.0.0-20190830083331-035f6764e8d2 // indirect
25- github.com/microcosm-cc/bluemonday v1.0.27 // indirect
26 github.com/muesli/cancelreader v0.2.2 // indirect
27 github.com/muesli/mango v0.2.0 // indirect
28 github.com/muesli/mango-pflag v0.2.0 // indirect
29@@ -89,7 +90,6 @@ require (
30 github.com/sahilm/fuzzy v0.1.3 // indirect
31 github.com/spf13/pflag v1.0.10 // indirect
32 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
33- github.com/yuin/goldmark v1.8.5 // indirect
34 github.com/yuin/goldmark-emoji v1.0.6 // indirect
35 golang.org/x/exp v0.0.0-20260727155853-b88d891fe743 // indirect
36 golang.org/x/net v0.57.0 // indirect
37diff --git a/pkg/web/pages/access.go b/pkg/web/pages/access.go
38new file mode 100644
39index 0000000000000000000000000000000000000000..7892b73a89d3e94191cd6a93ee0d389eb636af58
40--- /dev/null
41+++ b/pkg/web/pages/access.go
42@@ -0,0 +1,70 @@
43+package pages
44+
45+import (
46+ "context"
47+ "errors"
48+ "sort"
49+
50+ "github.com/charmbracelet/soft-serve/pkg/access"
51+ "github.com/charmbracelet/soft-serve/pkg/backend"
52+ "github.com/charmbracelet/soft-serve/pkg/proto"
53+)
54+
55+// ErrNotFound is returned when a repository must not be disclosed to a page visitor.
56+var ErrNotFound = errors.New("page resource not found")
57+
58+// IsPublicReadable reports whether repo may be shown to an anonymous page visitor.
59+func IsPublicReadable(repo proto.Repository, level access.AccessLevel) bool {
60+ return repo != nil && !repo.IsPrivate() && level >= access.ReadOnlyAccess
61+}
62+
63+// PublicRepository loads a repository only when it is publicly readable.
64+func PublicRepository(ctx context.Context, name string) (proto.Repository, error) {
65+ be := backend.FromContext(ctx)
66+ if be == nil {
67+ return nil, ErrNotFound
68+ }
69+
70+ repo, err := be.Repository(ctx, name)
71+ if err != nil || !IsPublicReadable(repo, be.AccessLevelForUser(ctx, name, nil)) {
72+ return nil, ErrNotFound
73+ }
74+ return repo, nil
75+}
76+
77+// HomepageRepositories returns visible public repositories in display order.
78+func HomepageRepositories(ctx context.Context) ([]proto.Repository, error) {
79+ be := backend.FromContext(ctx)
80+ if be == nil {
81+ return nil, ErrNotFound
82+ }
83+
84+ repos, err := be.Repositories(ctx)
85+ if err != nil {
86+ return nil, err
87+ }
88+
89+ return FilterHomepageRepositories(repos, func(repo proto.Repository) bool {
90+ return IsPublicReadable(repo, be.AccessLevelForUser(ctx, repo.Name(), nil))
91+ }), nil
92+}
93+
94+// FilterHomepageRepositories applies homepage visibility rules and display order.
95+func FilterHomepageRepositories(repos []proto.Repository, canRead func(proto.Repository) bool) []proto.Repository {
96+ visible := make([]proto.Repository, 0, len(repos))
97+ for _, repo := range repos {
98+ if repo.Name() == ".soft-serve" || repo.IsHidden() || !canRead(repo) {
99+ continue
100+ }
101+ visible = append(visible, repo)
102+ }
103+
104+ sort.Slice(visible, func(i, j int) bool {
105+ left, right := visible[i], visible[j]
106+ if left.UpdatedAt().Equal(right.UpdatedAt()) {
107+ return left.Name() < right.Name()
108+ }
109+ return left.UpdatedAt().After(right.UpdatedAt())
110+ })
111+ return visible
112+}
113diff --git a/pkg/web/pages/content.go b/pkg/web/pages/content.go
114new file mode 100644
115index 0000000000000000000000000000000000000000..1c70034f649a499494e8a7d98cc6fe4b6f917385
116--- /dev/null
117+++ b/pkg/web/pages/content.go
118@@ -0,0 +1,99 @@
119+package pages
120+
121+import (
122+ "bytes"
123+ "errors"
124+ "fmt"
125+ "path"
126+ "strings"
127+
128+ "github.com/charmbracelet/soft-serve/git"
129+)
130+
131+const (
132+ // TextLimit is the maximum source size rendered in a page.
133+ TextLimit int64 = 1 << 20
134+ // RawLimit is the maximum source size served by the raw endpoint.
135+ RawLimit int64 = 16 << 20
136+)
137+
138+var (
139+ // ErrContentTooLarge means the entry was rejected before loading its blob.
140+ ErrContentTooLarge = errors.New("content exceeds page size limit")
141+ // ErrReadmeNotFound means no regular README blob was found.
142+ ErrReadmeNotFound = errors.New("readme not found")
143+)
144+
145+// BlobContent is a bounded blob and its binary classification.
146+type BlobContent struct {
147+ Bytes []byte
148+ Size int64
149+ Binary bool
150+}
151+
152+// LoadBlob checks an entry's Git-reported size before loading its bytes.
153+func LoadBlob(entry *git.TreeEntry, limit int64) (BlobContent, error) {
154+ if entry == nil || (!entry.IsBlob() && !entry.IsExec()) {
155+ return BlobContent{}, fmt.Errorf("entry is not a regular blob")
156+ }
157+ size := entry.Size()
158+ if size > limit {
159+ return BlobContent{}, ErrContentTooLarge
160+ }
161+ contents, err := entry.Contents()
162+ if err != nil {
163+ return BlobContent{}, err
164+ }
165+ // A Git object cannot change after its ID has been read, but keep the limit
166+ // invariant if a Git implementation reports an unexpected size.
167+ if int64(len(contents)) > limit {
168+ return BlobContent{}, ErrContentTooLarge
169+ }
170+ return BlobContent{Bytes: contents, Size: int64(len(contents)), Binary: IsBinary(contents)}, nil
171+}
172+
173+// IsBinary detects NUL bytes in the first Git-style sniff window of bounded data.
174+func IsBinary(contents []byte) bool {
175+ const sniffLength = 8000
176+ if len(contents) > sniffLength {
177+ contents = contents[:sniffLength]
178+ }
179+ return bytes.IndexByte(contents, 0) >= 0
180+}
181+
182+// Readme contains the bounded README data and its POSIX path.
183+type Readme struct {
184+ Path string
185+ BlobContent
186+}
187+
188+// LoadReadme searches root, .github, then docs for a regular README blob.
189+func LoadReadme(repo *git.Repository, ref *git.Reference) (*Readme, error) {
190+ for _, directory := range []string{"", ".github", "docs"} {
191+ tree, err := repo.TreePath(ref, directory)
192+ if err != nil {
193+ continue
194+ }
195+ entries, err := tree.Entries()
196+ if err != nil {
197+ continue
198+ }
199+ for _, entry := range entries {
200+ // IsBlob intentionally excludes executable blobs and symlinks. README
201+ // symlinks must not cause a second path lookup.
202+ if !entry.IsBlob() || !isReadmeName(entry.Name()) {
203+ continue
204+ }
205+ content, err := LoadBlob(entry, TextLimit)
206+ if err != nil {
207+ return nil, err
208+ }
209+ return &Readme{Path: path.Join(directory, entry.Name()), BlobContent: content}, nil
210+ }
211+ }
212+ return nil, ErrReadmeNotFound
213+}
214+
215+func isReadmeName(name string) bool {
216+ return strings.HasPrefix(strings.ToLower(name), "readme")
217+}
218diff --git a/pkg/web/pages/controller.go b/pkg/web/pages/controller.go
219index 08de5959f957947012717f8d7e8173de0eed24b0..b1f0f735abdacc9ea7b3183967fc3edaecf0b2c5 100644
220--- a/pkg/web/pages/controller.go
221+++ b/pkg/web/pages/controller.go
222@@ -27,6 +27,7 @@ func Controller(_ context.Context, r *mux.Router) {
223
224 func home(w http.ResponseWriter, r *http.Request) {
225 cfg := config.FromContext(r.Context())
226+ SetSecurityHeaders(w)
227 w.Header().Set("Content-Type", "text/html; charset=utf-8")
228 if err := pageTemplates.ExecuteTemplate(w, "home", struct {
229 ServerName string
230diff --git a/pkg/web/pages/primitives_test.go b/pkg/web/pages/primitives_test.go
231new file mode 100644
232index 0000000000000000000000000000000000000000..abdbf23653eec43bd37b6deb7d5c98528d8d7a67
233--- /dev/null
234+++ b/pkg/web/pages/primitives_test.go
235@@ -0,0 +1,183 @@
236+package pages
237+
238+import (
239+ "errors"
240+ "os"
241+ "os/exec"
242+ "path/filepath"
243+ "strings"
244+ "testing"
245+ "time"
246+
247+ "github.com/charmbracelet/soft-serve/git"
248+ "github.com/charmbracelet/soft-serve/pkg/access"
249+ "github.com/charmbracelet/soft-serve/pkg/proto"
250+)
251+
252+type testRepository struct {
253+ name string
254+ private bool
255+ hidden bool
256+ updated time.Time
257+}
258+
259+func (r testRepository) ID() int64 { return 0 }
260+func (r testRepository) Name() string { return r.name }
261+func (r testRepository) ProjectName() string { return "" }
262+func (r testRepository) Description() string { return "" }
263+func (r testRepository) IsPrivate() bool { return r.private }
264+func (r testRepository) IsMirror() bool { return false }
265+func (r testRepository) IsHidden() bool { return r.hidden }
266+func (r testRepository) UserID() int64 { return 0 }
267+func (r testRepository) CreatedAt() time.Time { return r.updated }
268+func (r testRepository) UpdatedAt() time.Time { return r.updated }
269+func (r testRepository) Open() (*git.Repository, error) { return nil, errors.New("not used") }
270+
271+func TestIsPublicReadable(t *testing.T) {
272+ public := testRepository{name: "public"}
273+ private := testRepository{name: "private", private: true}
274+ if !IsPublicReadable(public, access.ReadOnlyAccess) {
275+ t.Fatal("public read-only repository is not readable")
276+ }
277+ if IsPublicReadable(private, access.AdminAccess) {
278+ t.Fatal("private repository must not be page-readable")
279+ }
280+ if IsPublicReadable(public, access.NoAccess) {
281+ t.Fatal("anonymous no-access repository is readable")
282+ }
283+}
284+
285+func TestFilterHomepageRepositories(t *testing.T) {
286+ newer := time.Date(2026, 1, 2, 0, 0, 0, 0, time.UTC)
287+ input := []proto.Repository{
288+ testRepository{name: "zebra", updated: newer},
289+ testRepository{name: "alpha", updated: newer},
290+ testRepository{name: "older", updated: newer.Add(-time.Hour)},
291+ testRepository{name: "hidden", hidden: true, updated: newer},
292+ testRepository{name: ".soft-serve", updated: newer},
293+ testRepository{name: "unreadable", updated: newer},
294+ }
295+ visible := FilterHomepageRepositories(input, func(repo proto.Repository) bool {
296+ return repo.Name() != "unreadable"
297+ })
298+ got := make([]string, len(visible))
299+ for i, repo := range visible {
300+ got[i] = repo.Name()
301+ }
302+ want := []string{"alpha", "zebra", "older"}
303+ if strings.Join(got, ",") != strings.Join(want, ",") {
304+ t.Fatalf("visible repositories = %v, want %v", got, want)
305+ }
306+}
307+
308+func TestResolveRefAndValidation(t *testing.T) {
309+ repo := newTestRepository(t, map[string]string{"README.md": "hello"})
310+ runGit(t, repo.Path, "branch", "feature/with-slash")
311+
312+ ref, err := ResolveRef(repo, "refs/heads/feature/with-slash")
313+ if err != nil || ref.Name().String() != "refs/heads/feature/with-slash" {
314+ t.Fatalf("ResolveRef slash ref = %v, %v", ref, err)
315+ }
316+ if _, err := ResolveRef(repo, "feature/with-slash^{commit}"); !errors.Is(err, ErrInvalidRef) {
317+ t.Fatalf("ResolveRef arbitrary revision error = %v, want ErrInvalidRef", err)
318+ }
319+ for _, invalid := range []string{"", "abc", strings.Repeat("z", 40), strings.Repeat("a", 39)} {
320+ if invalid != "" && ValidCommitHash(invalid) {
321+ t.Fatalf("ValidCommitHash(%q) = true", invalid)
322+ }
323+ }
324+ if !ValidCommitHash(strings.Repeat("a", 40)) || !ValidCommitHash(strings.Repeat("B", 64)) {
325+ t.Fatal("valid complete object IDs rejected")
326+ }
327+ for _, invalid := range []string{"/etc", "../outside", "a/../../outside", "\x00name"} {
328+ if _, err := ValidateTreePath(invalid); !errors.Is(err, ErrInvalidTreePath) {
329+ t.Fatalf("ValidateTreePath(%q) error = %v", invalid, err)
330+ }
331+ }
332+ if got, err := ValidateTreePath("dir/../README.md"); err != nil || got != "README.md" {
333+ t.Fatalf("ValidateTreePath cleaned = %q, %v", got, err)
334+ }
335diff --git a/pkg/web/pages/raw.go b/pkg/web/pages/raw.go
336new file mode 100644
337index 0000000000000000000000000000000000000000..fb5110d32bb122260576476839046631b799cbc8
338--- /dev/null
339+++ b/pkg/web/pages/raw.go
340@@ -0,0 +1,44 @@
341+package pages
342+
343+import (
344+ "mime"
345+ "path"
346+ "strings"
347+)
348+
349+// RawMetadata describes how a raw blob may be sent without executable content.
350+type RawMetadata struct {
351+ ContentType string
352+ ContentDisposition string
353+ Inline bool
354+}
355+
356+var safeMediaTypes = map[string]string{
357+ ".avif": "image/avif",
358+ ".gif": "image/gif",
359+ ".jpeg": "image/jpeg",
360+ ".jpg": "image/jpeg",
361+ ".png": "image/png",
362+ ".webp": "image/webp",
363+ ".flac": "audio/flac",
364+ ".m4a": "audio/mp4",
365+ ".mp3": "audio/mpeg",
366+ ".ogg": "audio/ogg",
367+ ".opus": "audio/opus",
368+ ".wav": "audio/wav",
369+ ".m4v": "video/mp4",
370+ ".mp4": "video/mp4",
371+ ".ogv": "video/ogg",
372+ ".webm": "video/webm",
373+}
374+
375+// RawFileMetadata allows only known safe media types inline; all other files download.
376+func RawFileMetadata(filename string) RawMetadata {
377+ if mediaType, ok := safeMediaTypes[strings.ToLower(path.Ext(filename))]; ok {
378+ return RawMetadata{ContentType: mediaType, Inline: true}
379+ }
380+ return RawMetadata{
381+ ContentType: "application/octet-stream",
382+ ContentDisposition: mime.FormatMediaType("attachment", map[string]string{"filename": path.Base(filename)}),
383+ }
384+}
385diff --git a/pkg/web/pages/render.go b/pkg/web/pages/render.go
386new file mode 100644
387index 0000000000000000000000000000000000000000..49fdbe3eca4310af356156e7485d408ab5b71ffb
388--- /dev/null
389+++ b/pkg/web/pages/render.go
390@@ -0,0 +1,55 @@
391+package pages
392+
393+import (
394+ "bytes"
395+ "html/template"
396+ "net/http"
397+
398+ "github.com/alecthomas/chroma/v2"
399+ chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
400+ "github.com/alecthomas/chroma/v2/lexers"
401+ "github.com/alecthomas/chroma/v2/styles"
402+ "github.com/microcosm-cc/bluemonday"
403+ "github.com/yuin/goldmark"
404+)
405+
406+// SetSecurityHeaders applies the common security policy for dynamic page responses.
407+func SetSecurityHeaders(w http.ResponseWriter) {
408+ w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'none'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'")
409+ w.Header().Set("X-Content-Type-Options", "nosniff")
410+ w.Header().Set("Referrer-Policy", "same-origin")
411+}
412+
413+// RenderMarkdown renders Markdown with raw HTML disabled and sanitizes the result.
414+func RenderMarkdown(source []byte) (template.HTML, error) {
415+ var rendered bytes.Buffer
416+ if err := goldmark.Convert(source, &rendered); err != nil {
417+ return "", err
418+ }
419+ return template.HTML(bluemonday.UGCPolicy().SanitizeBytes(rendered.Bytes())), nil // #nosec G203 -- sanitizer output only
420+}
421+
422+// RenderSource highlights bounded source using Chroma's escaping HTML formatter.
423+func RenderSource(filename string, source []byte) (template.HTML, error) {
424+ lexer := lexers.Match(filename)
425+ if lexer == nil {
426+ lexer = lexers.Analyse(string(source))
427+ }
428+ if lexer == nil {
429+ lexer = lexers.Fallback
430+ }
431+ iterator, err := lexer.Tokenise(nil, string(source))
432+ if err != nil {
433+ return "", err
434+ }
435+
436+ var rendered bytes.Buffer
437+ formatter := chromahtml.New(chromahtml.WithClasses(true))
438+ if err := formatter.Format(&rendered, styles.Fallback, iterator); err != nil {
439+ return "", err
440+ }
441+ return template.HTML(rendered.String()), nil // #nosec G203 -- Chroma escapes source tokens
442+}
443+
444+// Compile-time checks keep renderer dependencies explicit.
445+var _ chroma.Formatter = chromahtml.New()
446diff --git a/pkg/web/pages/repository.go b/pkg/web/pages/repository.go
447new file mode 100644
448index 0000000000000000000000000000000000000000..5bceaa286ee59ffcf1178dda75c2d473368404fe
449--- /dev/null
450+++ b/pkg/web/pages/repository.go
451@@ -0,0 +1,115 @@
452+package pages
453+
454+import (
455+ "errors"
456+ "fmt"
457+ "path"
458+ "strings"
459+
460+ "github.com/charmbracelet/soft-serve/git"
461+)
462+
463+const maxTreeEntries = 1000
464+
465+var (
466+ // ErrInvalidRef is returned for a requested ref that is not an actual ref.
467+ ErrInvalidRef = errors.New("invalid reference")
468+ // ErrInvalidCommitHash is returned before looking up a malformed object ID.
469+ ErrInvalidCommitHash = errors.New("invalid commit hash")
470+ // ErrInvalidTreePath is returned when a path is not contained in a Git tree.
471+ ErrInvalidTreePath = errors.New("invalid tree path")
472+)
473+
474+// ResolveRef resolves an empty ref to HEAD or matches a full ref name exactly.
475+func ResolveRef(repo *git.Repository, requested string) (*git.Reference, error) {
476+ if requested == "" {
477+ return repo.HEAD()
478+ }
479+
480+ refs, err := repo.References()
481+ if err != nil {
482+ return nil, err
483+ }
484+ for _, ref := range refs {
485+ if ref.Name().String() == requested {
486+ return ref, nil
487+ }
488+ }
489+ return nil, ErrInvalidRef
490+}
491+
492+// ValidCommitHash accepts only complete SHA-1 or SHA-256 hexadecimal object IDs.
493+func ValidCommitHash(hash string) bool {
494+ if len(hash) != 40 && len(hash) != 64 {
495+ return false
496+ }
497+ for _, char := range hash {
498+ if !(char >= '0' && char <= '9') && !(char >= 'a' && char <= 'f') && !(char >= 'A' && char <= 'F') {
499+ return false
500+ }
501+ }
502+ return true
503+}
504+
505+// LookupCommit validates hash before asking Git to load the commit object.
506+func LookupCommit(repo *git.Repository, hash string) (*git.Commit, error) {
507+ if !ValidCommitHash(hash) {
508+ return nil, ErrInvalidCommitHash
509+ }
510+ return repo.CatFileCommit(hash)
511+}
512+
513+// ValidateTreePath converts a POSIX tree path to its canonical relative form.
514+func ValidateTreePath(treePath string) (string, error) {
515+ if treePath == "" || treePath == "." {
516+ return "", nil
517+ }
518+ if strings.ContainsRune(treePath, 0) || path.IsAbs(treePath) {
519+ return "", ErrInvalidTreePath
520+ }
521+ cleaned := path.Clean(treePath)
522+ if cleaned == ".." || strings.HasPrefix(cleaned, "../") {
523+ return "", ErrInvalidTreePath
524+ }
525+ if cleaned == "." {
526+ return "", nil
527+ }
528+ return cleaned, nil
529+}
530+
531+// TreeEntries returns at most 1,000 entries from a validated tree path.
532+func TreeEntries(repo *git.Repository, ref *git.Reference, treePath string) (git.Entries, bool, error) {
533+ cleaned, err := ValidateTreePath(treePath)
534+ if err != nil {
535+ return nil, false, err
536+ }
537+ tree, err := repo.TreePath(ref, cleaned)
538+ if err != nil {
539+ return nil, false, err
540+ }
541+ entries, err := tree.Entries()
542+ if err != nil {
543+ return nil, false, err
544+ }
545+ entries.Sort()
546+ if len(entries) > maxTreeEntries {
547+ return entries[:maxTreeEntries], true, nil
548+ }
549+ return entries, false, nil
550+}
551diff --git a/pkg/web/pages/urls.go b/pkg/web/pages/urls.go
552new file mode 100644
553index 0000000000000000000000000000000000000000..7a86e7b49da50b77998a0e1747bd06adac4a3e1d
554--- /dev/null
555+++ b/pkg/web/pages/urls.go
556@@ -0,0 +1,46 @@
557+package pages
558+
559+import "net/url"
560+
561+// PageURL builds a page operation URL without interpolating untrusted values.
562+func PageURL(repository, operation string, query url.Values) string {
563+ segments := append(splitRepositoryName(repository), "@", operation)
564+ u := (&url.URL{Path: "/"}).JoinPath(segments...)
565+ if query != nil {
566+ u.RawQuery = query.Encode()
567+ }
568+ return u.String()
569+}
570+
571+// TreeURL builds a tree-browser URL for a selected repository, ref, and path.
572+func TreeURL(repository, ref, treePath string) string {
573+ return PageURL(repository, "tree", url.Values{"ref": {ref}, "path": {treePath}})
574+}
575+
576+// RawURL builds a raw-file URL for a selected repository, ref, and path.
577+func RawURL(repository, ref, treePath string) string {
578+ return PageURL(repository, "raw", url.Values{"ref": {ref}, "path": {treePath}})
579+}
580+
581+func splitRepositoryName(name string) []string {
582+ // A repository name may be nested. URL.JoinPath escapes each component and
583+ // preserves only the deliberate repository separators.
584+ return splitNonEmpty(name)
585+}
586+
587+func splitNonEmpty(value string) []string {
588+ segments := make([]string, 0, 1)
589+ start := 0
590+ for i := range value {
591+ if value[i] == '/' {
592+ if i > start {
593+ segments = append(segments, value[start:i])
594+ }
595+ start = i + 1
596+ }
597+ }
598+ if start < len(value) {
599+ segments = append(segments, value[start:])
600+ }
601+ return segments
602+}