Lindenii Project Forge
Lint ipc
version: "2" linters: default: all disable: - depguard - wsl_v5 # tmp - wsl # tmp - unused # tmp - nonamedreturns - err113 # tmp - gochecknoinits # tmp - nlreturn # tmp - cyclop # tmp - gocognit # tmp - varnamelen # tmp - funlen # tmp - lll - mnd # tmp - revive # tmp - godox # tmp
- nestif # tmp
linters-settings: revive: rules: - name: error-strings disabled: true issues: max-issues-per-linter: 0 max-same-issues: 0
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> package git2c import (
"context"
"fmt" "net" "go.lindenii.runxiyu.org/forge/forged/internal/common/bare" ) // Client represents a connection to the git2d backend daemon. type Client struct { socketPath string conn net.Conn writer *bare.Writer reader *bare.Reader } // NewClient establishes a connection to a git2d socket and returns a new Client.
func NewClient(socketPath string) (*Client, error) { conn, err := net.Dial("unix", socketPath)
func NewClient(ctx context.Context, socketPath string) (*Client, error) { dialer := &net.Dialer{} //exhaustruct:ignore conn, err := dialer.DialContext(ctx, "unix", socketPath)
if err != nil { return nil, fmt.Errorf("git2d connection failed: %w", err) } writer := bare.NewWriter(conn) reader := bare.NewReader(conn) return &Client{ socketPath: socketPath, conn: conn, writer: writer, reader: reader, }, nil } // Close terminates the underlying socket connection.
func (c *Client) Close() error {
func (c *Client) Close() (err error) {
if c.conn != nil {
return c.conn.Close()
err = c.conn.Close() if err != nil { return fmt.Errorf("close underlying socket: %w", err) }
} return nil }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> package git2c import ( "encoding/hex" "errors" "fmt" "io" ) // CmdIndex requests a repository index from git2d and returns the list of commits // and the contents of a README file if available. func (c *Client) CmdIndex(repoPath string) ([]Commit, *FilenameContents, error) {
if err := c.writer.WriteData([]byte(repoPath)); err != nil {
err := c.writer.WriteData([]byte(repoPath)) if err != nil {
return nil, nil, fmt.Errorf("sending repo path failed: %w", err) }
if err := c.writer.WriteUint(1); err != nil {
err = c.writer.WriteUint(1) if err != nil {
return nil, nil, fmt.Errorf("sending command failed: %w", err) } status, err := c.reader.ReadUint() if err != nil { return nil, nil, fmt.Errorf("reading status failed: %w", err) } if status != 0 { return nil, nil, fmt.Errorf("git2d error: %d", status) } // README readmeRaw, err := c.reader.ReadData() if err != nil { readmeRaw = nil } readmeFilename := "README.md" // TODO readme := &FilenameContents{Filename: readmeFilename, Content: readmeRaw} // Commits var commits []Commit for { id, err := c.reader.ReadData() if err != nil { if errors.Is(err, io.EOF) { break } return nil, nil, fmt.Errorf("reading commit ID failed: %w", err) } title, _ := c.reader.ReadData() authorName, _ := c.reader.ReadData() authorEmail, _ := c.reader.ReadData() authorDate, _ := c.reader.ReadData() commits = append(commits, Commit{ Hash: hex.EncodeToString(id), Author: string(authorName), Email: string(authorEmail), Date: string(authorDate), Message: string(title), }) } return commits, readme, nil }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> package git2c import ( "errors" "fmt" "io" ) // CmdTreeRaw queries git2d for a tree or blob object at the given path within the repository. // It returns either a directory listing or the contents of a file. func (c *Client) CmdTreeRaw(repoPath, pathSpec string) ([]TreeEntry, string, error) {
if err := c.writer.WriteData([]byte(repoPath)); err != nil {
err := c.writer.WriteData([]byte(repoPath)) if err != nil {
return nil, "", fmt.Errorf("sending repo path failed: %w", err) }
if err := c.writer.WriteUint(2); err != nil {
err = c.writer.WriteUint(2) if err != nil {
return nil, "", fmt.Errorf("sending command failed: %w", err) }
if err := c.writer.WriteData([]byte(pathSpec)); err != nil {
err = c.writer.WriteData([]byte(pathSpec)) if err != nil {
return nil, "", fmt.Errorf("sending path failed: %w", err) } status, err := c.reader.ReadUint() if err != nil { return nil, "", fmt.Errorf("reading status failed: %w", err) } switch status { case 0: kind, err := c.reader.ReadUint() if err != nil { return nil, "", fmt.Errorf("reading object kind failed: %w", err) } switch kind { case 1: // Tree count, err := c.reader.ReadUint() if err != nil { return nil, "", fmt.Errorf("reading entry count failed: %w", err) } var files []TreeEntry for range count { typeCode, err := c.reader.ReadUint() if err != nil { return nil, "", fmt.Errorf("error reading entry type: %w", err) } mode, err := c.reader.ReadUint() if err != nil { return nil, "", fmt.Errorf("error reading entry mode: %w", err) } size, err := c.reader.ReadUint() if err != nil { return nil, "", fmt.Errorf("error reading entry size: %w", err) } name, err := c.reader.ReadData() if err != nil { return nil, "", fmt.Errorf("error reading entry name: %w", err) } files = append(files, TreeEntry{ Name: string(name), Mode: fmt.Sprintf("%06o", mode), Size: size, IsFile: typeCode == 2, IsSubtree: typeCode == 1, }) } return files, "", nil case 2: // Blob content, err := c.reader.ReadData() if err != nil && !errors.Is(err, io.EOF) { return nil, "", fmt.Errorf("error reading file content: %w", err) } return nil, string(content), nil default: return nil, "", fmt.Errorf("unknown kind: %d", kind) } case 3: return nil, "", fmt.Errorf("path not found: %s", pathSpec) default: return nil, "", fmt.Errorf("unknown status code: %d", status) } }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> // TODO: Make the C part report detailed error messages too package git2c import "errors" var (
Success error
ErrUnknown = errors.New("git2c: unknown error") ErrPath = errors.New("git2c: get tree entry by path failed") ErrRevparse = errors.New("git2c: revparse failed") ErrReadme = errors.New("git2c: no readme") ErrBlobExpected = errors.New("git2c: blob expected") ErrEntryToObject = errors.New("git2c: tree entry to object conversion failed") ErrBlobRawContent = errors.New("git2c: get blob raw content failed") ErrRevwalk = errors.New("git2c: revwalk failed") ErrRevwalkPushHead = errors.New("git2c: revwalk push head failed") ErrBareProto = errors.New("git2c: bare protocol error") ) func Perror(errno uint) error { switch errno { case 0:
return Success
return nil
case 3: return ErrPath case 4: return ErrRevparse case 5: return ErrReadme case 6: return ErrBlobExpected case 7: return ErrEntryToObject case 8: return ErrBlobRawContent case 9: return ErrRevwalk case 10: return ErrRevwalkPushHead case 11: return ErrBareProto } return ErrUnknown }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> package irc import (
"context"
"crypto/tls"
"fmt"
"log/slog" "net" "go.lindenii.runxiyu.org/forge/forged/internal/common/misc" ) // Config contains IRC connection and identity settings for the bot. // This should usually be a part of the primary config struct. type Config 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"` } // Bot represents an IRC bot client that handles events and allows for sending messages. type Bot struct {
// TODO: Use each config field instead of embedding Config here.
config *Config ircSendBuffered chan string ircSendDirectChan chan misc.ErrorBack[string] } // NewBot creates a new Bot instance using the provided configuration. func NewBot(c *Config) (b *Bot) { b = &Bot{ config: c,
}
} //exhaustruct:ignore
return } // Connect establishes a new IRC session and starts handling incoming and outgoing messages. // This method blocks until an error occurs or the connection is closed.
func (b *Bot) Connect() error {
func (b *Bot) Connect(ctx context.Context) error {
var err error var underlyingConn net.Conn if b.config.TLS {
underlyingConn, err = tls.Dial(b.config.Net, b.config.Addr, nil)
dialer := tls.Dialer{} //exhaustruct:ignore underlyingConn, err = dialer.DialContext(ctx, b.config.Net, b.config.Addr)
} else {
underlyingConn, err = net.Dial(b.config.Net, b.config.Addr)
dialer := net.Dialer{} //exhaustruct:ignore underlyingConn, err = dialer.DialContext(ctx, b.config.Net, b.config.Addr)
} if err != nil {
return err
return fmt.Errorf("dialing irc: %w", err)
}
defer underlyingConn.Close()
defer func() { _ = underlyingConn.Close() }()
conn := NewConn(underlyingConn) logAndWriteLn := func(s string) (n int, err error) { slog.Debug("irc tx", "line", s) return conn.WriteString(s + "\r\n") } _, err = logAndWriteLn("NICK " + b.config.Nick) if err != nil { return err } _, err = logAndWriteLn("USER " + b.config.User + " 0 * :" + b.config.Gecos) if err != nil { return err } readLoopError := make(chan error) writeLoopAbort := make(chan struct{}) go func() { for { select { case <-writeLoopAbort: return default: } msg, line, err := conn.ReadMessage() if err != nil { readLoopError <- err return } slog.Debug("irc rx", "line", line) switch msg.Command { case "001": _, err = logAndWriteLn("JOIN #chat") if err != nil { readLoopError <- err return } case "PING": _, err = logAndWriteLn("PONG :" + msg.Args[0]) if err != nil { readLoopError <- err return } case "JOIN": c, ok := msg.Source.(Client) if !ok { slog.Error("unable to convert source of JOIN to client") } if c.Nick != b.config.Nick { continue } default: } } }() for { select { case err = <-readLoopError: return err case line := <-b.ircSendBuffered: _, err = logAndWriteLn(line) if err != nil { select { case b.ircSendBuffered <- line: default: slog.Error("unable to requeue message", "line", line) } writeLoopAbort <- struct{}{} return err } case lineErrorBack := <-b.ircSendDirectChan: _, err = logAndWriteLn(lineErrorBack.Content) lineErrorBack.ErrorChan <- err if err != nil { writeLoopAbort <- struct{}{} return err } } } } // SendDirect sends an IRC message directly to the connection and bypasses // the buffering system. func (b *Bot) SendDirect(line string) error { ech := make(chan error, 1) b.ircSendDirectChan <- misc.ErrorBack[string]{ Content: line, ErrorChan: ech, } return <-ech } // Send queues a message to be sent asynchronously via the buffered send queue. // If the queue is full, the message is dropped and an error is logged. func (b *Bot) Send(line string) { select { case b.ircSendBuffered <- line: default: slog.Error("irc sendq full", "line", line) } } // ConnectLoop continuously attempts to maintain an IRC session. // If the connection drops, it automatically retries with no delay.
func (b *Bot) ConnectLoop() {
func (b *Bot) ConnectLoop(ctx context.Context) {
b.ircSendBuffered = make(chan string, b.config.SendQ) b.ircSendDirectChan = make(chan misc.ErrorBack[string]) for {
err := b.Connect()
err := b.Connect(ctx)
slog.Error("irc session error", "error", err) } }
package irc import ( "bufio"
"fmt"
"net" "slices" "go.lindenii.runxiyu.org/forge/forged/internal/common/misc" ) type Conn struct { netConn net.Conn bufReader *bufio.Reader } func NewConn(netConn net.Conn) Conn { return Conn{ netConn: netConn, bufReader: bufio.NewReader(netConn), } } func (c *Conn) ReadMessage() (msg Message, line string, err error) { raw, err := c.bufReader.ReadSlice('\n') if err != nil { return } if raw[len(raw)-1] == '\n' { raw = raw[:len(raw)-1] } if raw[len(raw)-1] == '\r' { raw = raw[:len(raw)-1] } lineBytes := slices.Clone(raw) line = misc.BytesToString(lineBytes) msg, err = Parse(lineBytes) return } func (c *Conn) Write(p []byte) (n int, err error) {
return c.netConn.Write(p)
n, err = c.netConn.Write(p) if err != nil { err = fmt.Errorf("write to connection: %w", err) } return n, err
} func (c *Conn) WriteString(s string) (n int, err error) {
return c.netConn.Write(misc.StringToBytes(s))
n, err = c.netConn.Write(misc.StringToBytes(s)) if err != nil { err = fmt.Errorf("write to connection: %w", err) } return n, err
}
// SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: Copyright (c) 2018-2024 luk3yx <https://luk3yx.github.io> // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> package irc import ( "bytes" "go.lindenii.runxiyu.org/forge/forged/internal/common/misc" ) type Message struct { Command string Source Source Tags map[string]string Args []string } // All strings returned are borrowed from the input byte slice. func Parse(raw []byte) (msg Message, err error) { sp := bytes.Split(raw, []byte{' '}) // TODO: Use bytes.Cut instead here if bytes.HasPrefix(sp[0], []byte{'@'}) { // TODO: Check size manually if len(sp[0]) < 2 { err = ErrMalformedMsg
return
return msg, err
} sp[0] = sp[0][1:] msg.Tags, err = tagsToMap(sp[0]) if err != nil {
return
return msg, err
} if len(sp) < 2 { err = ErrMalformedMsg
return
return msg, err
} sp = sp[1:] } else { msg.Tags = nil // TODO: Is a nil map the correct thing to use here? } if bytes.HasPrefix(sp[0], []byte{':'}) { // TODO: Check size manually if len(sp[0]) < 2 { err = ErrMalformedMsg
return
return msg, err
} sp[0] = sp[0][1:] msg.Source = parseSource(sp[0]) if len(sp) < 2 { err = ErrMalformedMsg
return
return msg, err
} sp = sp[1:] } msg.Command = misc.BytesToString(sp[0]) if len(sp) < 2 {
return
return msg, err
} sp = sp[1:] for i := 0; i < len(sp); i++ { if len(sp[i]) == 0 { continue } if sp[i][0] == ':' { if len(sp[i]) < 2 { sp[i] = []byte{} } else { sp[i] = sp[i][1:] } msg.Args = append(msg.Args, misc.BytesToString(bytes.Join(sp[i:], []byte{' '}))) // TODO: Avoid Join by not using sp in the first place break } msg.Args = append(msg.Args, misc.BytesToString(sp[i])) }
return
return msg, err
} var ircv3TagEscapes = map[byte]byte{ //nolint:gochecknoglobals ':': ';', 's': ' ', 'r': '\r', 'n': '\n', } func tagsToMap(raw []byte) (tags map[string]string, err error) { tags = make(map[string]string) for rawTag := range bytes.SplitSeq(raw, []byte{';'}) { key, value, found := bytes.Cut(rawTag, []byte{'='}) if !found { err = ErrInvalidIRCv3Tag
return
return tags, err
} if len(value) == 0 { tags[misc.BytesToString(key)] = "" } else { if !bytes.Contains(value, []byte{'\\'}) { tags[misc.BytesToString(key)] = misc.BytesToString(value) } else { valueUnescaped := bytes.NewBuffer(make([]byte, 0, len(value))) for i := 0; i < len(value); i++ { if value[i] == '\\' { i++ byteUnescaped, ok := ircv3TagEscapes[value[i]] if !ok { byteUnescaped = value[i] } valueUnescaped.WriteByte(byteUnescaped) } else { valueUnescaped.WriteByte(value[i]) } } tags[misc.BytesToString(key)] = misc.BytesToString(valueUnescaped.Bytes()) } } }
return
return tags, err
}
// SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> package irc import ( "bytes" "go.lindenii.runxiyu.org/forge/forged/internal/common/misc" ) type Source interface { AsSourceString() string }
//nolint:ireturn
func parseSource(s []byte) Source { nick, userhost, found := bytes.Cut(s, []byte{'!'}) if !found { return Server{name: misc.BytesToString(s)} } user, host, found := bytes.Cut(userhost, []byte{'@'}) if !found { return Server{name: misc.BytesToString(s)} } return Client{ Nick: misc.BytesToString(nick), User: misc.BytesToString(user), Host: misc.BytesToString(host), } } type Server struct { name string } func (s Server) AsSourceString() string { return s.name } type Client struct { Nick string User string Host string } func (c Client) AsSourceString() string { return c.Nick + "!" + c.User + "@" + c.Host }