Hi… I am well aware that this diff view is very suboptimal. It will be fixed when the refactored server comes along!
Update README
# Lindenii Forge
**Work in progress.**
Lindenii Forge has been superseded. Further development will be on Villosa, which is temporarily hosted on [Codeberg](https://codeberg.org/lindenii/villosa). The `master` branch of Lindenii Forge doesn't have much. If you want to read the code of, I suppose most active instances of Lindenii Forge, see the `pre-refactor` branch. ---
Lindenii Forge aims to be an uncomplicated yet featured software forge, primarily designed for self-hosting by small organizations and individuals. * [Upstream source repository](https://forge.lindenii.runxiyu.org/forge/-/repos/server/) ([backup](https://git.lindenii.runxiyu.org/forge.git/)) * [Website and documentation](https://lindenii.runxiyu.org/forge/) * [Temporary issue tracker](https://todo.sr.ht/~runxiyu/forge) * IRC [`#lindenii`](https://webirc.runxiyu.org/kiwiirc/#lindenii) on [irc.runxiyu.org](https://irc.runxiyu.org)\ and [`#lindenii`](https://web.libera.chat/#lindenii) on [Libera.Chat](https://libera.chat) ## Implemented features * Umambiguously parsable URL * Groups and subgroups * Repo hosting * Push to `contrib/` branches to automatically create merge requests * Basic federated authentication * Converting mailed patches to branches ## Planned features * Further integration with mailing list workflows * Further federated authentication * Ticket trackers, discussions, RFCs * Web interface * Email integration with IMAP archives * SSH API * Email access * CI system similar to builds.sr.ht ## License We are currently using the [GNU Affero General Public License version 3](https://www.gnu.org/licenses/agpl-3.0.html). The forge software serves its own source at `/-/source/`. ## Contribute Please submit patches by pushing to `contrib/...` in the official repo. Alternatively, send email to [`forge/-/repos/server@forge.lindenii.runxiyu.org`](mailto:forge%2F-%2Frepos%2Fserver@forge.lindenii.runxiyu.org). Note that emailing patches is still experimental. ## Mirrors We have several repo mirrors: * [Official repo on our own instance of Lindenii Forge](https://forge.lindenii.org/forge/-/repos/server/) * [The Lindenii Project's backup cgit](https://git.lindenii.org/forge.git/) * [SourceHut](https://git.sr.ht/~runxiyu/forge/) * [GitHub](https://github.com/runxiyu/forge/) ## Architecture We have a mostly monolithic server `forged` written in Go. PostgreSQL is used to store everything other than Git repositories. Git repositories currently must be accessible via the local filesystem from the machine running `forged`, since `forged` currently uses `go-git`, `git2d` via UNIX domain sockets, and `git-upload-pack`/`git-receive-pack` subprocesses. In the future, `git2d` will be expanded to support all operations, removing our dependence on `git-upload-pack`/`git-receive-pack` and `go-git`; `git2d` will also be extended to support remote IPC via a custom RPC protocol, likely based on SCTP (with TLS via RFC 3436). ## `git2d` `git2d` is a Git server daemon written in C, which uses `libgit2` to handle Git operations. ```c int cmd_index(git_repository * repo, struct bare_writer *writer); int cmd_treeraw(git_repository * repo, struct bare_reader *reader, struct bare_writer *writer); int cmd_resolve_ref(git_repository * repo, struct bare_reader *reader, struct bare_writer *writer); int cmd_list_branches(git_repository * repo, struct bare_writer *writer); int cmd_format_patch(git_repository * repo, struct bare_reader *reader, struct bare_writer *writer); int cmd_merge_base(git_repository * repo, struct bare_reader *reader, struct bare_writer *writer); int cmd_log(git_repository * repo, struct bare_reader *reader, struct bare_writer *writer); int cmd_tree_list_by_oid(git_repository * repo, struct bare_reader *reader, struct bare_writer *writer); int cmd_write_tree(git_repository * repo, struct bare_reader *reader, struct bare_writer *writer); int cmd_blob_write(git_repository * repo, struct bare_reader *reader, struct bare_writer *writer); int cmd_commit_tree_oid(git_repository * repo, struct bare_reader *reader, struct bare_writer *writer); int cmd_commit_create(git_repository * repo, struct bare_reader *reader, struct bare_writer *writer); int cmd_update_ref(git_repository * repo, struct bare_reader *reader, struct bare_writer *writer); int cmd_commit_info(git_repository * repo, struct bare_reader *reader, struct bare_writer *writer); int cmd_init_repo(const char *path, struct bare_reader *reader, struct bare_writer *writer); ``` We are planning to rewrite `git2d` in Hare, using [`hare-git`](https://forge.lindenii.org/hare/-/repos/hare-git/) when it's ready.
// SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: Copyright (c) 2020 Simon Ser <https://emersion.fr>
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
package scfg
package scfgs
import (
"bufio"
"fmt"
"io"
"os"
"strings"
)
// This limits the max block nesting depth to prevent stack overflows.
const maxNestingDepth = 1000
// Load loads a configuration file.
func Load(path string) (block Block, err error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer func() {
if cerr := f.Close(); err == nil && cerr != nil {
err = cerr
}
}()
return Read(f)
}
// Read parses a configuration file from an io.Reader.
func Read(r io.Reader) (Block, error) {
scanner := bufio.NewScanner(r)
dec := decoder{scanner: scanner}
block, closingBrace, err := dec.readBlock()
if err != nil {
return nil, err
} else if closingBrace {
return nil, fmt.Errorf("line %v: unexpected '}'", dec.lineno)
}
return block, scanner.Err()
}
type decoder struct {
scanner *bufio.Scanner
lineno int
blockDepth int
}
// readBlock reads a block. closingBrace is true if parsing stopped on '}'
// (otherwise, it stopped on Scanner.Scan).
func (dec *decoder) readBlock() (block Block, closingBrace bool, err error) {
dec.blockDepth++
defer func() {
dec.blockDepth--
}()
if dec.blockDepth >= maxNestingDepth {
return nil, false, fmt.Errorf("exceeded max block depth")
}
for dec.scanner.Scan() {
dec.lineno++
l := dec.scanner.Text()
words, err := splitWords(l)
if err != nil {
return nil, false, fmt.Errorf("line %v: %v", dec.lineno, err)
} else if len(words) == 0 {
continue
}
if len(words) == 1 && l[len(l)-1] == '}' {
closingBrace = true
break
}
var d *Directive
if words[len(words)-1] == "{" && l[len(l)-1] == '{' {
words = words[:len(words)-1]
var name string
params := words
if len(words) > 0 {
name, params = words[0], words[1:]
}
startLineno := dec.lineno
childBlock, childClosingBrace, err := dec.readBlock()
if err != nil {
return nil, false, err
} else if !childClosingBrace {
return nil, false, fmt.Errorf("line %v: unterminated block", startLineno)
}
// Allows callers to tell apart "no block" and "empty block"
if childBlock == nil {
childBlock = Block{}
}
d = &Directive{Name: name, Params: params, Children: childBlock, lineno: dec.lineno}
} else {
d = &Directive{Name: words[0], Params: words[1:], lineno: dec.lineno}
}
block = append(block, d)
}
return block, closingBrace, nil
}
func splitWords(l string) ([]string, error) {
var (
words []string
sb strings.Builder
escape bool
quote rune
wantWSP bool
)
for _, ch := range l {
switch {
case escape:
sb.WriteRune(ch)
escape = false
case wantWSP && (ch != ' ' && ch != '\t'):
return words, fmt.Errorf("atom not allowed after quoted string")
case ch == '\\':
escape = true
case quote != 0 && ch == quote:
quote = 0
wantWSP = true
if sb.Len() == 0 {
words = append(words, "")
}
case quote == 0 && len(words) == 0 && sb.Len() == 0 && ch == '#':
return nil, nil
case quote == 0 && (ch == '\'' || ch == '"'):
if sb.Len() > 0 {
return words, fmt.Errorf("quoted string not allowed after atom")
}
quote = ch
case quote == 0 && (ch == ' ' || ch == '\t'):
if sb.Len() > 0 {
words = append(words, sb.String())
}
sb.Reset()
wantWSP = false
default:
sb.WriteRune(ch)
}
}
if quote != 0 {
return words, fmt.Errorf("unterminated quoted string")
}
if sb.Len() > 0 {
words = append(words, sb.String())
}
return words, nil
}
// SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: Copyright (c) 2020 Simon Ser <https://emersion.fr>
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
// Package scfg parses and formats configuration files. // Note that this fork of scfg behaves differently from upstream scfg. package scfg
// Package scfgs parses and formats configuration files. // Note that this fork of scfgs behaves differently from upstream scfg. package scfgs
import (
"fmt"
)
// Block is a list of directives.
type Block []*Directive
// GetAll returns a list of directives with the provided name.
func (blk Block) GetAll(name string) []*Directive {
l := make([]*Directive, 0, len(blk))
for _, child := range blk {
if child.Name == name {
l = append(l, child)
}
}
return l
}
// Get returns the first directive with the provided name.
func (blk Block) Get(name string) *Directive {
for _, child := range blk {
if child.Name == name {
return child
}
}
return nil
}
// Directive is a configuration directive.
type Directive struct {
Name string
Params []string
Children Block
lineno int
}
// ParseParams extracts parameters from the directive. It errors out if the
// user hasn't provided enough parameters.
func (d *Directive) ParseParams(params ...*string) error {
if len(d.Params) < len(params) {
return fmt.Errorf("directive %q: want %v params, got %v", d.Name, len(params), len(d.Params))
}
for i, ptr := range params {
if ptr == nil {
continue
}
*ptr = d.Params[i]
}
return nil
}
// SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: Copyright (c) 2020 Simon Ser <https://emersion.fr>
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
package scfg
package scfgs
import ( "fmt" "reflect" "strings" "sync" )
// structInfo contains scfg metadata for structs.
// structInfo contains scfgs metadata for structs.
type structInfo struct {
param int // index of field storing parameters
children map[string]int // indices of fields storing child directives
}
var (
structCacheMutex sync.Mutex
structCache = make(map[reflect.Type]*structInfo)
)
func getStructInfo(t reflect.Type) (*structInfo, error) {
structCacheMutex.Lock()
defer structCacheMutex.Unlock()
if info := structCache[t]; info != nil {
return info, nil
}
info := &structInfo{
param: -1,
children: make(map[string]int),
}
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
if f.Anonymous {
return nil, fmt.Errorf("scfg: anonymous struct fields are not supported")
return nil, fmt.Errorf("scfgs: anonymous struct fields are not supported")
} else if !f.IsExported() {
continue
}
tag := f.Tag.Get("scfg")
tag := f.Tag.Get("scfgs")
parts := strings.Split(tag, ",")
k, options := parts[0], parts[1:]
if k == "-" {
continue
} else if k == "" {
k = f.Name
}
isParam := false
for _, opt := range options {
switch opt {
case "param":
isParam = true
default:
return nil, fmt.Errorf("scfg: invalid option %q in struct tag", opt)
return nil, fmt.Errorf("scfgs: invalid option %q in struct tag", opt)
}
}
if isParam {
if info.param >= 0 {
return nil, fmt.Errorf("scfg: param option specified multiple times in struct tag in %v", t)
return nil, fmt.Errorf("scfgs: param option specified multiple times in struct tag in %v", t)
}
if parts[0] != "" {
return nil, fmt.Errorf("scfg: name must be empty when param option is specified in struct tag in %v", t)
return nil, fmt.Errorf("scfgs: name must be empty when param option is specified in struct tag in %v", t)
}
info.param = i
} else {
if _, ok := info.children[k]; ok {
return nil, fmt.Errorf("scfg: key %q specified multiple times in struct tag in %v", k, t)
return nil, fmt.Errorf("scfgs: key %q specified multiple times in struct tag in %v", k, t)
} info.children[k] = i } } structCache[t] = info return info, nil }
// SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: Copyright (c) 2020 Simon Ser <https://emersion.fr> // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
package scfg
package scfgs
import ( "encoding" "fmt" "io" "reflect" "strconv" )
// Decoder reads and decodes an scfg document from an input stream.
// Decoder reads and decodes an scfgs document from an input stream.
type Decoder struct {
r io.Reader
unknownDirectives []*Directive
}
// NewDecoder returns a new decoder which reads from r.
func NewDecoder(r io.Reader) *Decoder {
return &Decoder{r: r}
}
// UnknownDirectives returns a slice of all unknown directives encountered
// during Decode.
func (dec *Decoder) UnknownDirectives() []*Directive {
return dec.unknownDirectives
}
// Decode reads scfg document from the input and stores it in the value pointed
// Decode reads scfgs document from the input and stores it in the value pointed
// to by v. // // If v is nil or not a pointer, Decode returns an error. // // Blocks can be unmarshaled to: // // - Maps. Each directive is unmarshaled into a map entry. The map key must // be a string. // - Structs. Each directive is unmarshaled into a struct field. // // Duplicate directives are not allowed, unless the struct field or map value // is a slice of values representing a directive: structs or maps. // // Directives can be unmarshaled to: // // - Maps. The children block is unmarshaled into the map. Parameters are not // allowed. // - Structs. The children block is unmarshaled into the struct. Parameters // are allowed if one of the struct fields contains the "param" option in // its tag. // - Slices. Parameters are unmarshaled into the slice. Children blocks are // not allowed. // - Arrays. Parameters are unmarshaled into the array. The number of // parameters must match exactly the length of the array. Children blocks // are not allowed. // - Strings, booleans, integers, floating-point values, values implementing // encoding.TextUnmarshaler. Only a single parameter is allowed and is // unmarshaled into the value. Children blocks are not allowed. // // The decoding of each struct field can be customized by the format string
// stored under the "scfg" key in the struct field's tag. The tag contains the
// stored under the "scfgs" key in the struct field's tag. The tag contains the
// name of the field possibly followed by a comma-separated list of options.
// The name may be empty in order to specify options without overriding the
// default field name. As a special case, if the field name is "-", the field
// is ignored. The "param" option specifies that directive parameters are
// stored in this field (the name must be empty).
func (dec *Decoder) Decode(v interface{}) error {
block, err := Read(dec.r)
if err != nil {
return err
}
rv := reflect.ValueOf(v)
if rv.Kind() != reflect.Ptr || rv.IsNil() {
return fmt.Errorf("scfg: invalid value for unmarshaling")
return fmt.Errorf("scfgs: invalid value for unmarshaling")
}
return dec.unmarshalBlock(block, rv)
}
func (dec *Decoder) unmarshalBlock(block Block, v reflect.Value) error {
v = unwrapPointers(v)
t := v.Type()
dirsByName := make(map[string][]*Directive, len(block))
for _, dir := range block {
dirsByName[dir.Name] = append(dirsByName[dir.Name], dir)
}
switch v.Kind() {
case reflect.Map:
if t.Key().Kind() != reflect.String {
return fmt.Errorf("scfg: map key type must be string")
return fmt.Errorf("scfgs: map key type must be string")
}
if v.IsNil() {
v.Set(reflect.MakeMap(t))
} else if v.Len() > 0 {
clearMap(v)
}
for name, dirs := range dirsByName {
mv := reflect.New(t.Elem()).Elem()
if err := dec.unmarshalDirectiveList(dirs, mv); err != nil {
return err
}
v.SetMapIndex(reflect.ValueOf(name), mv)
}
case reflect.Struct:
si, err := getStructInfo(t)
if err != nil {
return err
}
seen := make(map[int]bool)
for name, dirs := range dirsByName {
fieldIndex, ok := si.children[name]
if !ok {
dec.unknownDirectives = append(dec.unknownDirectives, dirs...)
continue
}
fv := v.Field(fieldIndex)
if err := dec.unmarshalDirectiveList(dirs, fv); err != nil {
return err
}
seen[fieldIndex] = true
}
for name, fieldIndex := range si.children {
if fieldIndex == si.param {
continue
}
if _, ok := seen[fieldIndex]; !ok {
return fmt.Errorf("scfg: missing required directive %q", name)
return fmt.Errorf("scfgs: missing required directive %q", name)
} } default:
return fmt.Errorf("scfg: unsupported type for unmarshaling blocks: %v", t)
return fmt.Errorf("scfgs: unsupported type for unmarshaling blocks: %v", t)
}
return nil
}
func (dec *Decoder) unmarshalDirectiveList(dirs []*Directive, v reflect.Value) error {
v = unwrapPointers(v)
t := v.Type()
if v.Kind() != reflect.Slice || !isDirectiveType(t.Elem()) {
if len(dirs) > 1 {
return newUnmarshalDirectiveError(dirs[1], "directive must not be specified more than once")
}
return dec.unmarshalDirective(dirs[0], v)
}
sv := reflect.MakeSlice(t, len(dirs), len(dirs))
for i, dir := range dirs {
if err := dec.unmarshalDirective(dir, sv.Index(i)); err != nil {
return err
}
}
v.Set(sv)
return nil
}
// isDirectiveType checks whether a type can only be unmarshaled as a
// directive, not as a parameter. Accepting too many types here would result in
// ambiguities, see:
// https://lists.sr.ht/~emersion/public-inbox/%3C20230629132458.152205-1-contact%40emersion.fr%3E#%3Ch4Y2peS_YBqY3ar4XlmPDPiNBFpYGns3EBYUx3_6zWEhV2o8_-fBQveRujGADWYhVVCucHBEryFGoPtpC3d3mQ-x10pWnFogfprbQTSvtxc=@emersion.fr%3E
func isDirectiveType(t reflect.Type) bool {
for t.Kind() == reflect.Ptr {
t = t.Elem()
}
textUnmarshalerType := reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem()
if reflect.PointerTo(t).Implements(textUnmarshalerType) {
return false
}
switch t.Kind() {
case reflect.Struct, reflect.Map:
return true
default:
return false
}
}
func (dec *Decoder) unmarshalDirective(dir *Directive, v reflect.Value) error {
v = unwrapPointers(v)
t := v.Type()
if v.CanAddr() {
if _, ok := v.Addr().Interface().(encoding.TextUnmarshaler); ok {
if len(dir.Children) != 0 {
return newUnmarshalDirectiveError(dir, "directive requires zero children")
}
return unmarshalParamList(dir, v)
}
}
switch v.Kind() {
case reflect.Map:
if len(dir.Params) > 0 {
return newUnmarshalDirectiveError(dir, "directive requires zero parameters")
}
if err := dec.unmarshalBlock(dir.Children, v); err != nil {
return err
}
case reflect.Struct:
si, err := getStructInfo(t)
if err != nil {
return err
}
if si.param >= 0 {
if err := unmarshalParamList(dir, v.Field(si.param)); err != nil {
return err
}
} else {
if len(dir.Params) > 0 {
return newUnmarshalDirectiveError(dir, "directive requires zero parameters")
}
}
if err := dec.unmarshalBlock(dir.Children, v); err != nil {
return err
}
default:
if len(dir.Children) != 0 {
return newUnmarshalDirectiveError(dir, "directive requires zero children")
}
if err := unmarshalParamList(dir, v); err != nil {
return err
}
}
return nil
}
func unmarshalParamList(dir *Directive, v reflect.Value) error {
switch v.Kind() {
case reflect.Slice:
t := v.Type()
sv := reflect.MakeSlice(t, len(dir.Params), len(dir.Params))
for i, param := range dir.Params {
if err := unmarshalParam(param, sv.Index(i)); err != nil {
return newUnmarshalParamError(dir, i, err)
}
}
v.Set(sv)
case reflect.Array:
if len(dir.Params) != v.Len() {
return newUnmarshalDirectiveError(dir, fmt.Sprintf("directive requires exactly %v parameters", v.Len()))
}
for i, param := range dir.Params {
if err := unmarshalParam(param, v.Index(i)); err != nil {
return newUnmarshalParamError(dir, i, err)
}
}
default:
if len(dir.Params) != 1 {
return newUnmarshalDirectiveError(dir, "directive requires exactly one parameter")
}
if err := unmarshalParam(dir.Params[0], v); err != nil {
return newUnmarshalParamError(dir, 0, err)
}
}
return nil
}
func unmarshalParam(param string, v reflect.Value) error {
v = unwrapPointers(v)
t := v.Type()
// TODO: improve our logic following:
// https://cs.opensource.google/go/go/+/refs/tags/go1.21.5:src/encoding/json/decode.go;drc=b9b8cecbfc72168ca03ad586cc2ed52b0e8db409;l=421
if v.CanAddr() {
if v, ok := v.Addr().Interface().(encoding.TextUnmarshaler); ok {
return v.UnmarshalText([]byte(param))
}
}
switch v.Kind() {
case reflect.String:
v.Set(reflect.ValueOf(param))
case reflect.Bool:
switch param {
case "true":
v.Set(reflect.ValueOf(true))
case "false":
v.Set(reflect.ValueOf(false))
default:
return fmt.Errorf("invalid bool parameter %q", param)
}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
i, err := strconv.ParseInt(param, 10, t.Bits())
if err != nil {
return fmt.Errorf("invalid %v parameter: %v", t, err)
}
v.Set(reflect.ValueOf(i).Convert(t))
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
u, err := strconv.ParseUint(param, 10, t.Bits())
if err != nil {
return fmt.Errorf("invalid %v parameter: %v", t, err)
}
v.Set(reflect.ValueOf(u).Convert(t))
case reflect.Float32, reflect.Float64:
f, err := strconv.ParseFloat(param, t.Bits())
if err != nil {
return fmt.Errorf("invalid %v parameter: %v", t, err)
}
v.Set(reflect.ValueOf(f).Convert(t))
default:
return fmt.Errorf("unsupported type for unmarshaling parameter: %v", t)
}
return nil
}
func unwrapPointers(v reflect.Value) reflect.Value {
for v.Kind() == reflect.Ptr {
if v.IsNil() {
v.Set(reflect.New(v.Type().Elem()))
}
v = v.Elem()
}
return v
}
func clearMap(v reflect.Value) {
for _, k := range v.MapKeys() {
v.SetMapIndex(k, reflect.Value{})
}
}
type unmarshalDirectiveError struct {
lineno int
name string
msg string
}
func newUnmarshalDirectiveError(dir *Directive, msg string) *unmarshalDirectiveError {
return &unmarshalDirectiveError{
name: dir.Name,
lineno: dir.lineno,
msg: msg,
}
}
func (err *unmarshalDirectiveError) Error() string {
return fmt.Sprintf("line %v, directive %q: %v", err.lineno, err.name, err.msg)
}
type unmarshalParamError struct {
lineno int
directive string
paramIndex int
err error
}
func newUnmarshalParamError(dir *Directive, paramIndex int, err error) *unmarshalParamError {
return &unmarshalParamError{
directive: dir.Name,
lineno: dir.lineno,
paramIndex: paramIndex,
err: err,
}
}
func (err *unmarshalParamError) Error() string {
return fmt.Sprintf("line %v, directive %q, parameter %v: %v", err.lineno, err.directive, err.paramIndex+1, err.err)
}
// SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: Copyright (c) 2020 Simon Ser <https://emersion.fr>
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
package scfg
package scfgs
import ( "errors" "io" "strings" )
var errDirEmptyName = errors.New("scfg: directive with empty name")
var errDirEmptyName = errors.New("scfgs: directive with empty name")
// Write writes a parsed configuration to the provided io.Writer.
func Write(w io.Writer, blk Block) error {
enc := newEncoder(w)
err := enc.encodeBlock(blk)
return err
}
// encoder write SCFG directives to an output stream.
type encoder struct {
w io.Writer
lvl int
err error
}
// newEncoder returns a new encoder that writes to w.
func newEncoder(w io.Writer) *encoder {
return &encoder{w: w}
}
func (enc *encoder) push() {
enc.lvl++
}
func (enc *encoder) pop() {
enc.lvl--
}
func (enc *encoder) writeIndent() {
for i := 0; i < enc.lvl; i++ {
enc.write([]byte("\t"))
}
}
func (enc *encoder) write(p []byte) {
if enc.err != nil {
return
}
_, enc.err = enc.w.Write(p)
}
func (enc *encoder) encodeBlock(blk Block) error {
for _, dir := range blk {
if err := enc.encodeDir(*dir); err != nil {
return err
}
}
return enc.err
}
func (enc *encoder) encodeDir(dir Directive) error {
if enc.err != nil {
return enc.err
}
if dir.Name == "" {
enc.err = errDirEmptyName
return enc.err
}
enc.writeIndent()
enc.write([]byte(maybeQuote(dir.Name)))
for _, p := range dir.Params {
enc.write([]byte(" "))
enc.write([]byte(maybeQuote(p)))
}
if len(dir.Children) > 0 {
enc.write([]byte(" {\n"))
enc.push()
if err := enc.encodeBlock(dir.Children); err != nil {
return err
}
enc.pop()
enc.writeIndent()
enc.write([]byte("}"))
}
enc.write([]byte("\n"))
return enc.err
}
const specialChars = "\"\\\r\n'{} \t"
func maybeQuote(s string) string {
if s == "" || strings.ContainsAny(s, specialChars) {
var sb strings.Builder
sb.WriteByte('"')
for _, ch := range s {
if strings.ContainsRune(`"\`, ch) {
sb.WriteByte('\\')
}
sb.WriteRune(ch)
}
sb.WriteByte('"')
return sb.String()
}
return s
}
package config
import (
"bufio"
"fmt"
"log/slog"
"os"
"go.lindenii.runxiyu.org/forge/forged/internal/common/scfg"
)
type Config struct {
DB DB `scfg:"db"` Web Web `scfg:"web"` Hooks Hooks `scfg:"hooks"` LMTP LMTP `scfg:"lmtp"` SSH SSH `scfg:"ssh"` IRC IRC `scfg:"irc"` Git Git `scfg:"git"` General General `scfg:"general"` Pprof Pprof `scfg:"pprof"`
DB DB `scfgs:"db"` Web Web `scfgs:"web"` Hooks Hooks `scfgs:"hooks"` LMTP LMTP `scfgs:"lmtp"` SSH SSH `scfgs:"ssh"` IRC IRC `scfgs:"irc"` Git Git `scfgs:"git"` General General `scfgs:"general"` Pprof Pprof `scfgs:"pprof"`
}
type DB struct {
Conn string `scfg:"conn"`
Conn string `scfgs:"conn"`
}
type Web struct {
Net string `scfg:"net"` Addr string `scfg:"addr"` Root string `scfg:"root"` CookieExpiry int `scfg:"cookie_expiry"` ReadTimeout uint32 `scfg:"read_timeout"` WriteTimeout uint32 `scfg:"write_timeout"` IdleTimeout uint32 `scfg:"idle_timeout"` MaxHeaderBytes int `scfg:"max_header_bytes"` ReverseProxy bool `scfg:"reverse_proxy"` ShutdownTimeout uint32 `scfg:"shutdown_timeout"` TemplatesPath string `scfg:"templates_path"` StaticPath string `scfg:"static_path"`
Net string `scfgs:"net"` Addr string `scfgs:"addr"` Root string `scfgs:"root"` CookieExpiry int `scfgs:"cookie_expiry"` ReadTimeout uint32 `scfgs:"read_timeout"` WriteTimeout uint32 `scfgs:"write_timeout"` IdleTimeout uint32 `scfgs:"idle_timeout"` MaxHeaderBytes int `scfgs:"max_header_bytes"` ReverseProxy bool `scfgs:"reverse_proxy"` ShutdownTimeout uint32 `scfgs:"shutdown_timeout"` TemplatesPath string `scfgs:"templates_path"` StaticPath string `scfgs:"static_path"`
}
type Hooks struct {
Socket string `scfg:"socket"` Execs string `scfg:"execs"`
Socket string `scfgs:"socket"` Execs string `scfgs:"execs"`
}
type LMTP struct {
Socket string `scfg:"socket"` Domain string `scfg:"domain"` MaxSize int64 `scfg:"max_size"` WriteTimeout uint32 `scfg:"write_timeout"` ReadTimeout uint32 `scfg:"read_timeout"`
Socket string `scfgs:"socket"` Domain string `scfgs:"domain"` MaxSize int64 `scfgs:"max_size"` WriteTimeout uint32 `scfgs:"write_timeout"` ReadTimeout uint32 `scfgs:"read_timeout"`
}
type SSH struct {
Net string `scfg:"net"` Addr string `scfg:"addr"` Key string `scfg:"key"` Root string `scfg:"root"` ShutdownTimeout uint32 `scfg:"shutdown_timeout"`
Net string `scfgs:"net"` Addr string `scfgs:"addr"` Key string `scfgs:"key"` Root string `scfgs:"root"` ShutdownTimeout uint32 `scfgs:"shutdown_timeout"`
}
type IRC struct {
Net string `scfg:"net"` Addr string `scfg:"addr"` TLS bool `scfg:"tls"` SendQ uint `scfg:"sendq"` Nick string `scfg:"nick"` User string `scfg:"user"` Gecos string `scfg:"gecos"`
Net string `scfgs:"net"` Addr string `scfgs:"addr"` TLS bool `scfgs:"tls"` SendQ uint `scfgs:"sendq"` Nick string `scfgs:"nick"` User string `scfgs:"user"` Gecos string `scfgs:"gecos"`
}
type Git struct {
RepoDir string `scfg:"repo_dir"` Socket string `scfg:"socket"`
RepoDir string `scfgs:"repo_dir"` Socket string `scfgs:"socket"`
}
type General struct {
Title string `scfg:"title"`
Title string `scfgs:"title"`
}
type Pprof struct {
Net string `scfg:"net"` Addr string `scfg:"addr"`
Net string `scfgs:"net"` Addr string `scfgs:"addr"`
}
func Open(path string) (config Config, err error) {
var configFile *os.File
configFile, err = os.Open(path) //#nosec G304
if err != nil {
err = fmt.Errorf("open config file: %w", err)
return config, err
}
defer func() {
_ = configFile.Close()
}()
decoder := scfg.NewDecoder(bufio.NewReader(configFile))
decoder := scfgs.NewDecoder(bufio.NewReader(configFile))
err = decoder.Decode(&config)
if err != nil {
err = fmt.Errorf("decode config file: %w", err)
return config, err
}
for _, u := range decoder.UnknownDirectives() {
slog.Warn("unknown configuration directive", "directive", u)
}
return config, err
}
module go.lindenii.runxiyu.org/forge go 1.24.1 require ( github.com/gliderlabs/ssh v0.3.8
github.com/jackc/pgx/v5 v5.7.5
github.com/jackc/pgx/v5 v5.7.6
github.com/yuin/goldmark v1.7.13
golang.org/x/crypto v0.41.0 golang.org/x/sync v0.16.0
golang.org/x/crypto v0.43.0 golang.org/x/sync v0.17.0
) require ( github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/stretchr/testify v1.10.0 // indirect
golang.org/x/sys v0.35.0 // indirect golang.org/x/text v0.28.0 // indirect
golang.org/x/sys v0.37.0 // indirect golang.org/x/text v0.30.0 // indirect
)
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs= github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk= github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA= github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=