// 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 . 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) }