Lindenii Project Forge
Login

server

Lindenii Forge’s main backend daemon

Hi… I am well aware that this diff view is very suboptimal. It will be fixed when the refactored server comes along!

Commit info
ID
db03e1a2a84b5edfd95ce190c849eb5afa5a464e
Author
Runxi Yu <me@runxiyu.org>
Author date
Wed, 12 Nov 2025 00:31:04 +0800
Committer
Runxi Yu <me@runxiyu.org>
Committer date
Wed, 12 Nov 2025 00:31:04 +0800
Actions
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=