Cthulhu is no longer a fork. Now it is a screen reader written in Golang.
This commit is contained in:
1 parent
a998fe67de
commit
069fc674dc
708 files changed
+112482
-532
No files matched your search
@@ -0,0 +1,231 @@
|
||||
// Cthulhu screen reader.
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify it
|
||||
// under the terms of the GNU General Public License as published by the Free
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>.
|
||||
|
||||
package logging
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const redactedValue = "[REDACTED]"
|
||||
|
||||
type Options struct {
|
||||
Level slog.Level
|
||||
AddSource bool
|
||||
SensitiveKeys []string
|
||||
}
|
||||
|
||||
type Handler struct {
|
||||
writer io.Writer
|
||||
writerLock *sync.Mutex
|
||||
level slog.Level
|
||||
addSource bool
|
||||
sensitiveKeys map[string]struct{}
|
||||
group string
|
||||
attrs []slog.Attr
|
||||
}
|
||||
|
||||
func New(writer io.Writer, options Options) *slog.Logger {
|
||||
return slog.New(newHandler(writer, options))
|
||||
}
|
||||
|
||||
func newHandler(writer io.Writer, options Options) *Handler {
|
||||
if writer == nil {
|
||||
writer = io.Discard
|
||||
}
|
||||
level := options.Level
|
||||
if level == 0 {
|
||||
level = slog.LevelInfo
|
||||
}
|
||||
sensitiveKeys := map[string]struct{}{}
|
||||
for _, key := range append(defaultSensitiveKeys(), options.SensitiveKeys...) {
|
||||
sensitiveKeys[lowercaseKey(key)] = struct{}{}
|
||||
}
|
||||
return &Handler{
|
||||
writer: writer,
|
||||
writerLock: &sync.Mutex{},
|
||||
level: level,
|
||||
addSource: options.AddSource,
|
||||
sensitiveKeys: sensitiveKeys,
|
||||
}
|
||||
}
|
||||
|
||||
func (handler *Handler) Enabled(_ context.Context, level slog.Level) bool {
|
||||
return level >= handler.level
|
||||
}
|
||||
|
||||
func (handler *Handler) Handle(_ context.Context, record slog.Record) error {
|
||||
_, err := handler.write(handler.format(record))
|
||||
return err
|
||||
}
|
||||
|
||||
func (handler *Handler) format(record slog.Record) []byte {
|
||||
var buffer bytes.Buffer
|
||||
buffer.WriteByte('{')
|
||||
appendField(&buffer, "message", record.Message)
|
||||
appendField(&buffer, "level", record.Level.String())
|
||||
if handler.addSource && record.PC != 0 {
|
||||
frame, _ := runtime.CallersFrames([]uintptr{record.PC}).Next()
|
||||
appendField(&buffer, "source", fmt.Sprintf("%s:%d", frame.File, frame.Line))
|
||||
appendField(&buffer, "function", frame.Function)
|
||||
}
|
||||
|
||||
for _, attr := range handler.attrs {
|
||||
handler.appendAttr(&buffer, attr, "")
|
||||
}
|
||||
record.Attrs(func(attr slog.Attr) bool {
|
||||
handler.appendAttr(&buffer, attr, handler.group)
|
||||
return true
|
||||
})
|
||||
|
||||
appendField(&buffer, "time", record.Time.Format(time.RFC3339Nano))
|
||||
|
||||
buffer.WriteByte('}')
|
||||
buffer.WriteByte('\n')
|
||||
return buffer.Bytes()
|
||||
}
|
||||
|
||||
func (handler *Handler) write(data []byte) (int, error) {
|
||||
handler.writerLock.Lock()
|
||||
defer handler.writerLock.Unlock()
|
||||
return handler.writer.Write(data)
|
||||
}
|
||||
|
||||
func (handler *Handler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||
clone := *handler
|
||||
clone.attrs = append([]slog.Attr(nil), handler.attrs...)
|
||||
prefix := ""
|
||||
if handler.group != "" {
|
||||
prefix = handler.group + "."
|
||||
}
|
||||
for _, attr := range attrs {
|
||||
if prefix == "" {
|
||||
clone.attrs = append(clone.attrs, attr)
|
||||
continue
|
||||
}
|
||||
clone.attrs = append(clone.attrs, slog.Attr{Key: prefix + attr.Key, Value: attr.Value})
|
||||
}
|
||||
return &clone
|
||||
}
|
||||
|
||||
func (handler *Handler) WithGroup(name string) slog.Handler {
|
||||
if name == "" {
|
||||
return handler
|
||||
}
|
||||
clone := *handler
|
||||
if handler.group == "" {
|
||||
clone.group = name
|
||||
} else {
|
||||
clone.group = handler.group + "." + name
|
||||
}
|
||||
return &clone
|
||||
}
|
||||
|
||||
func (handler *Handler) appendAttr(buffer *bytes.Buffer, attr slog.Attr, prefix string) {
|
||||
attr.Value = attr.Value.Resolve()
|
||||
if attr.Equal(slog.Attr{}) {
|
||||
return
|
||||
}
|
||||
if attr.Value.Kind() == slog.KindGroup {
|
||||
groupPrefix := prefix
|
||||
if attr.Key != "" && prefix != "" {
|
||||
groupPrefix = prefix + "." + attr.Key
|
||||
} else if attr.Key != "" {
|
||||
groupPrefix = attr.Key
|
||||
}
|
||||
for _, child := range attr.Value.Group() {
|
||||
handler.appendAttr(buffer, child, groupPrefix)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
key := attr.Key
|
||||
if prefix != "" {
|
||||
key = prefix + "." + attr.Key
|
||||
}
|
||||
var value any
|
||||
if attr.Value.Kind() == slog.KindString {
|
||||
value = attr.Value.String()
|
||||
} else {
|
||||
value = attr.Value.Any()
|
||||
}
|
||||
if handler.isSensitive(key) {
|
||||
value = redactedValue
|
||||
} else if err, ok := value.(error); ok {
|
||||
value = err.Error()
|
||||
}
|
||||
appendField(buffer, key, value)
|
||||
}
|
||||
|
||||
func (handler *Handler) isSensitive(key string) bool {
|
||||
normalized := lowercaseKey(key)
|
||||
if _, exists := handler.sensitiveKeys[normalized]; exists {
|
||||
return true
|
||||
}
|
||||
lastDot := len(normalized) - 1
|
||||
for lastDot >= 0 && normalized[lastDot] != '.' {
|
||||
lastDot--
|
||||
}
|
||||
_, exists := handler.sensitiveKeys[normalized[lastDot+1:]]
|
||||
return exists
|
||||
}
|
||||
|
||||
func appendField(buffer *bytes.Buffer, key string, value any) {
|
||||
if buffer.Len() > 1 {
|
||||
buffer.WriteByte(',')
|
||||
}
|
||||
encodedKey, err := json.Marshal(key)
|
||||
if err != nil {
|
||||
encodedKey = []byte(fmt.Sprintf("%q", key))
|
||||
}
|
||||
encodedValue, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
encodedValue = []byte(fmt.Sprintf("%q", fmt.Sprint(value)))
|
||||
}
|
||||
buffer.Write(encodedKey)
|
||||
buffer.WriteByte(':')
|
||||
buffer.Write(encodedValue)
|
||||
}
|
||||
|
||||
func defaultSensitiveKeys() []string {
|
||||
return []string{
|
||||
"authorization",
|
||||
"document_text",
|
||||
"host",
|
||||
"hostname",
|
||||
"password",
|
||||
"private_text",
|
||||
"protected_text",
|
||||
"secret",
|
||||
"token",
|
||||
"url",
|
||||
"username",
|
||||
}
|
||||
}
|
||||
|
||||
func lowercaseKey(key string) string {
|
||||
return strings.ToLower(key)
|
||||
}
|
||||
Reference in new issue
Block a user