mirror of
https://github.com/ivuorinen/gibidify.git
synced 2026-03-19 04:02:37 +00:00
feat: update go to 1.25, add permissions and envs (#49)
* chore(ci): update go to 1.25, add permissions and envs * fix(ci): update pr-lint.yml * chore: update go, fix linting * fix: tests and linting * fix(lint): lint fixes, renovate should now pass * fix: updates, security upgrades * chore: workflow updates, lint * fix: more lint, checkmake, and other fixes * fix: more lint, convert scripts to POSIX compliant * fix: simplify codeql workflow * tests: increase test coverage, fix found issues * fix(lint): editorconfig checking, add to linters * fix(lint): shellcheck, add to linters * fix(lint): apply cr comment suggestions * fix(ci): remove step-security/harden-runner * fix(lint): remove duplication, apply cr fixes * fix(ci): tests in CI/CD pipeline * chore(lint): deduplication of strings * fix(lint): apply cr comment suggestions * fix(ci): actionlint * fix(lint): apply cr comment suggestions * chore: lint, add deps management
This commit is contained in:
162
gibidiutils/writers.go
Normal file
162
gibidiutils/writers.go
Normal file
@@ -0,0 +1,162 @@
|
||||
// Package gibidiutils provides common utility functions for gibidify.
|
||||
package gibidiutils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"math"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SafeCloseReader safely closes a reader if it implements io.Closer.
|
||||
// This eliminates the duplicated closeReader methods across all writers.
|
||||
func SafeCloseReader(reader io.Reader, path string) {
|
||||
if closer, ok := reader.(io.Closer); ok {
|
||||
if err := closer.Close(); err != nil {
|
||||
LogError(
|
||||
"Failed to close file reader",
|
||||
WrapError(err, ErrorTypeIO, CodeIOClose, "failed to close file reader").WithFilePath(path),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WriteWithErrorWrap performs file writing with consistent error handling.
|
||||
// This centralizes the common pattern of writing strings with error wrapping.
|
||||
func WriteWithErrorWrap(writer io.Writer, content, errorMsg, filePath string) error {
|
||||
if _, err := writer.Write([]byte(content)); err != nil {
|
||||
wrappedErr := WrapError(err, ErrorTypeIO, CodeIOWrite, errorMsg)
|
||||
if filePath != "" {
|
||||
wrappedErr = wrappedErr.WithFilePath(filePath)
|
||||
}
|
||||
return wrappedErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// StreamContent provides a common streaming implementation with chunk processing.
|
||||
// This eliminates the similar streaming patterns across JSON and Markdown writers.
|
||||
//
|
||||
//revive:disable-next-line:cognitive-complexity
|
||||
func StreamContent(
|
||||
reader io.Reader,
|
||||
writer io.Writer,
|
||||
chunkSize int,
|
||||
filePath string,
|
||||
processChunk func([]byte) []byte,
|
||||
) error {
|
||||
buf := make([]byte, chunkSize)
|
||||
for {
|
||||
n, err := reader.Read(buf)
|
||||
if n > 0 {
|
||||
processed := buf[:n]
|
||||
if processChunk != nil {
|
||||
processed = processChunk(processed)
|
||||
}
|
||||
if _, writeErr := writer.Write(processed); writeErr != nil {
|
||||
wrappedErr := WrapError(writeErr, ErrorTypeIO, CodeIOWrite, "failed to write content chunk")
|
||||
if filePath != "" {
|
||||
wrappedErr = wrappedErr.WithFilePath(filePath)
|
||||
}
|
||||
return wrappedErr
|
||||
}
|
||||
}
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
wrappedErr := WrapError(err, ErrorTypeIO, CodeIOFileRead, "failed to read content chunk")
|
||||
if filePath != "" {
|
||||
wrappedErr = wrappedErr.WithFilePath(filePath)
|
||||
}
|
||||
return wrappedErr
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EscapeForJSON escapes content for JSON output using the standard library.
|
||||
// This replaces the custom escapeJSONString function with a more robust implementation.
|
||||
func EscapeForJSON(content string) string {
|
||||
// Use the standard library's JSON marshaling for proper escaping
|
||||
jsonBytes, _ := json.Marshal(content)
|
||||
// Remove the surrounding quotes that json.Marshal adds
|
||||
jsonStr := string(jsonBytes)
|
||||
if len(jsonStr) >= 2 && jsonStr[0] == '"' && jsonStr[len(jsonStr)-1] == '"' {
|
||||
return jsonStr[1 : len(jsonStr)-1]
|
||||
}
|
||||
return jsonStr
|
||||
}
|
||||
|
||||
// EscapeForYAML quotes/escapes content for YAML output if needed.
|
||||
// This centralizes the YAML string quoting logic.
|
||||
func EscapeForYAML(content string) string {
|
||||
// Quote if contains special characters, spaces, or starts with special chars
|
||||
needsQuotes := strings.ContainsAny(content, " \t\n\r:{}[]|>-'\"\\") ||
|
||||
strings.HasPrefix(content, "-") ||
|
||||
strings.HasPrefix(content, "?") ||
|
||||
strings.HasPrefix(content, ":") ||
|
||||
content == "" ||
|
||||
content == "true" || content == "false" ||
|
||||
content == "null" || content == "~"
|
||||
|
||||
if needsQuotes {
|
||||
// Use double quotes and escape internal quotes
|
||||
escaped := strings.ReplaceAll(content, "\\", "\\\\")
|
||||
escaped = strings.ReplaceAll(escaped, "\"", "\\\"")
|
||||
return "\"" + escaped + "\""
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
// SafeUint64ToInt64WithDefault safely converts uint64 to int64, returning a default value if overflow would occur.
|
||||
// When defaultValue is 0 (the safe default), clamps to MaxInt64 on overflow to keep guardrails active.
|
||||
// This prevents overflow from making monitors think memory usage is zero when it's actually maxed out.
|
||||
func SafeUint64ToInt64WithDefault(value uint64, defaultValue int64) int64 {
|
||||
if value > math.MaxInt64 {
|
||||
// When caller uses 0 as "safe" default, clamp to max so overflow still trips guardrails
|
||||
if defaultValue == 0 {
|
||||
return math.MaxInt64
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
return int64(value) //#nosec G115 -- Safe: value <= MaxInt64 checked above
|
||||
}
|
||||
|
||||
// StreamLines provides line-based streaming for YAML content.
|
||||
// This provides an alternative streaming approach for YAML writers.
|
||||
func StreamLines(reader io.Reader, writer io.Writer, filePath string, lineProcessor func(string) string) error {
|
||||
// Read all content first (for small files this is fine)
|
||||
content, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
wrappedErr := WrapError(err, ErrorTypeIO, CodeIOFileRead, "failed to read content for line processing")
|
||||
if filePath != "" {
|
||||
wrappedErr = wrappedErr.WithFilePath(filePath)
|
||||
}
|
||||
return wrappedErr
|
||||
}
|
||||
|
||||
// Split into lines and process each
|
||||
lines := strings.Split(string(content), "\n")
|
||||
for i, line := range lines {
|
||||
processedLine := line
|
||||
if lineProcessor != nil {
|
||||
processedLine = lineProcessor(line)
|
||||
}
|
||||
|
||||
// Write line with proper line ending (except for last empty line)
|
||||
lineToWrite := processedLine
|
||||
if i < len(lines)-1 || line != "" {
|
||||
lineToWrite += "\n"
|
||||
}
|
||||
|
||||
if _, writeErr := writer.Write([]byte(lineToWrite)); writeErr != nil {
|
||||
wrappedErr := WrapError(writeErr, ErrorTypeIO, CodeIOWrite, "failed to write processed line")
|
||||
if filePath != "" {
|
||||
wrappedErr = wrappedErr.WithFilePath(filePath)
|
||||
}
|
||||
return wrappedErr
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user