1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
package main

import (
	"fmt"
	"path"
	"strings"
)

// Per-directory metadata: the ".source-forge" file
// ============================================================================
//
// A directory may carry a checked-in ".source-forge" file describing how that
// directory's own listing is displayed. It is read during ingest (the file is
// already in the tar stream, so no extra git call) and its effects are baked
// into the listing table, exactly like the automatic collapse rule; nothing is
// parsed at request time.
//
// The format is one "key = value" per line, "#" comments and blank lines
// ignored, split on the FIRST "=" (so a value may contain further "="). Keys
// that take a list are simply repeated — there is no comma-separated form, so
// no value ever has to worry about containing a comma:
//
//	# ./.source-forge
//	description = Profpatsch's personal monorepo.
//
//	shortcut = users/Profpatsch
//	shortcut = users/Profpatsch/git-blimey
//
// Keys:
//
//	shortcut     Repeatable, allowed in any directory. Adds an extra entry to
//	             this directory's listing pointing at a descendant, spliced in
//	             next to the entry it lives under. Purely additive: it can only
//	             ever add links, never remove or replace one, so no shortcut
//	             can make part of the tree unreachable.
//	description  Once per file. One line of inline HTML describing the
//	             directory the file sits in, shown on that directory's entry in
//	             every listing it appears in and above its own listing. At the
//	             root, where there is no parent listing, it is the project's
//	             blurb instead, shown on the project page and the site index.
//	             That is the only way to set it, so deleting the line and
//	             pushing clears it (see storeDescriptions in ingest.go).
//
//	             It is HTML rather than markdown because of where it lands: on
//	             the entry's own line, inside the listing's <span>, where only
//	             phrasing content is valid. Markdown's natural output is a <p>
//	             block, which a browser will not accept there. It is also
//	             stored verbatim and emitted UNESCAPED — see the description
//	             columns' comments in schema.go for why that trust boundary is
//	             accepted.
//
// Everything here is best-effort: a bad line, an unknown key or an
// unresolvable target produces a warning on stderr naming the file and line,
// and that line alone is ignored. Ingest is never failed by it, because the
// post-receive hook runs AFTER git has already moved the ref: aborting would
// leave the repository updated but the published site stuck on the previous
// generation, which is a far worse outcome than one ignored directive. The
// warning does reach whoever pushed, since git relays hook stderr back over
// the wire as "remote: source-forge: ...".

// metaFileName is the per-directory metadata file, read at ingest.
const metaFileName = ".source-forge"

// dirMeta holds the directives parsed from one directory's .source-forge.
type dirMeta struct {
	dir string // directory the file lives in ("" for the repo root)

	// shortcuts are extra listing entries, as paths relative to dir, in the
	// order they were declared.
	shortcuts []string
	// description is one line of inline HTML describing dir itself. Empty
	// means "not declared".
	description string
}

// warnf reports a problem with a metadata file and carries on. Called for
// every ignored line; see the package comment above for why nothing here is
// fatal.
func warnf(file string, line int, format string, args ...any) {
	fmt.Fprintf(stderr, "source-forge: %s:%d: %s\n",
		file, line, fmt.Sprintf(format, args...))
}

// parseDirMeta parses the contents of a .source-forge file living in dir.
//
// Syntactically bad lines and unknown keys are warned about and skipped, so
// the result is always usable; the targets named by shortcuts are NOT checked
// here (that needs the whole tree, which is only complete once the tar stream
// has been read — see validShortcuts).
func parseDirMeta(dir string, content []byte) *dirMeta {
	m := &dirMeta{dir: dir}
	file := path.Join(dir, metaFileName)

	for i, raw := range strings.Split(string(content), "\n") {
		lineno := i + 1
		line := strings.TrimSpace(raw)
		if line == "" || strings.HasPrefix(line, "#") {
			continue
		}
		// Split on the first "=" only, so a value may contain more of them.
		key, value, ok := strings.Cut(line, "=")
		if !ok {
			warnf(file, lineno, "ignoring line: expected 'key = value', got %q", line)
			continue
		}
		key = strings.TrimSpace(key)
		value = strings.TrimSpace(value)
		if value == "" {
			warnf(file, lineno, "ignoring %q: empty value", key)
			continue
		}

		switch key {
		case "shortcut":
			m.shortcuts = append(m.shortcuts, value)
		case "description":
			// Repeating it is a mistake rather than a list: keep the first, so
			// the warning points at the line that was added rather than
			// silently letting the last one win.
			if m.description != "" {
				warnf(file, lineno, "ignoring repeated 'description' (keeping the first one)")
				continue
			}
			m.description = value
		default:
			warnf(file, lineno, "ignoring unknown key %q", key)
		}
	}
	return m
}

// validShortcuts resolves this directory's shortcut targets to full tree
// paths, dropping (with a warning) every one that cannot be displayed.
//
// A target must exist in the tree and be a strict descendant of the declaring
// directory: it may be neither absolute nor contain "." or ".." segments,
// which also rules out naming the directory itself. A shortcut only ever ADDS
// an entry to the listing, so pointing it at the directory (already listed by
// its parent) or outside the tree would be meaningless rather than a way to
// reshape the listing.
//
// exists reports whether a tree path is present in the ingested tree.
func (m *dirMeta) validShortcuts(exists func(string) bool) []string {
	if len(m.shortcuts) == 0 {
		return nil
	}
	file := path.Join(m.dir, metaFileName)

	var out []string
	seen := map[string]bool{}
	for _, rel := range m.shortcuts {
		// Line numbers are not tracked per shortcut; identify by value, which
		// is what the reader has to go and fix anyway.
		warn := func(format string, args ...any) {
			fmt.Fprintf(stderr, "source-forge: %s: ignoring shortcut %q: %s\n",
				file, rel, fmt.Sprintf(format, args...))
		}

		if strings.HasPrefix(rel, "/") {
			warn("must be relative to %q, not absolute", dirLabel(m.dir))
			continue
		}
		// path.Join cleans "." and ".." away, so check the raw segments: a
		// shortcut that escapes its directory, or names it, must be rejected
		// rather than silently clamped to something else. Together with the
		// checks above (and the parser's rejection of an empty value) this is
		// also what guarantees the target is a STRICT descendant: every
		// remaining input adds at least one non-"."/".." segment to m.dir.
		if hasDotSegment(rel) {
			warn("must not contain '.' or '..' path segments")
			continue
		}
		target := path.Join(m.dir, rel)
		if !exists(target) {
			warn("no such file or directory in the tree")
			continue
		}
		if seen[target] {
			warn("already listed")
			continue
		}
		seen[target] = true
		out = append(out, target)
	}
	return out
}

// hasDotSegment reports whether any segment of a relative path is "." or "..".
func hasDotSegment(rel string) bool {
	for _, seg := range strings.Split(rel, "/") {
		if seg == "." || seg == ".." {
			return true
		}
	}
	return false
}

// dirLabel names a directory for humans, spelling the root (the empty path) as
// something that reads sensibly in a message.
func dirLabel(dir string) string {
	if dir == "" {
		return "the repository root"
	}
	return dir
}