mirror of
https://github.com/ivuorinen/gibidify.git
synced 2026-03-10 07:59:21 +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:
@@ -4,11 +4,13 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/ivuorinen/gibidify/utils"
|
||||
"github.com/ivuorinen/gibidify/gibidiutils"
|
||||
)
|
||||
|
||||
// MarkdownWriter handles markdown format output with streaming support.
|
||||
// MarkdownWriter handles Markdown format output with streaming support.
|
||||
type MarkdownWriter struct {
|
||||
outFile *os.File
|
||||
}
|
||||
@@ -19,16 +21,21 @@ func NewMarkdownWriter(outFile *os.File) *MarkdownWriter {
|
||||
}
|
||||
|
||||
// Start writes the markdown header.
|
||||
func (w *MarkdownWriter) Start(prefix, suffix string) error {
|
||||
func (w *MarkdownWriter) Start(prefix, _ string) error {
|
||||
if prefix != "" {
|
||||
if _, err := fmt.Fprintf(w.outFile, "# %s\n\n", prefix); err != nil {
|
||||
return utils.WrapError(err, utils.ErrorTypeIO, utils.CodeIOWrite, "failed to write prefix")
|
||||
return gibidiutils.WrapError(
|
||||
err,
|
||||
gibidiutils.ErrorTypeIO,
|
||||
gibidiutils.CodeIOWrite,
|
||||
"failed to write prefix",
|
||||
)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteFile writes a file entry in markdown format.
|
||||
// WriteFile writes a file entry in Markdown format.
|
||||
func (w *MarkdownWriter) WriteFile(req WriteRequest) error {
|
||||
if req.IsStream {
|
||||
return w.writeStreaming(req)
|
||||
@@ -40,21 +47,99 @@ func (w *MarkdownWriter) WriteFile(req WriteRequest) error {
|
||||
func (w *MarkdownWriter) Close(suffix string) error {
|
||||
if suffix != "" {
|
||||
if _, err := fmt.Fprintf(w.outFile, "\n# %s\n", suffix); err != nil {
|
||||
return utils.WrapError(err, utils.ErrorTypeIO, utils.CodeIOWrite, "failed to write suffix")
|
||||
return gibidiutils.WrapError(
|
||||
err,
|
||||
gibidiutils.ErrorTypeIO,
|
||||
gibidiutils.CodeIOWrite,
|
||||
"failed to write suffix",
|
||||
)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateMarkdownPath validates a file path for markdown output.
|
||||
func validateMarkdownPath(path string) error {
|
||||
trimmed := strings.TrimSpace(path)
|
||||
if trimmed == "" {
|
||||
return gibidiutils.NewStructuredError(
|
||||
gibidiutils.ErrorTypeValidation,
|
||||
gibidiutils.CodeValidationRequired,
|
||||
"file path cannot be empty",
|
||||
"",
|
||||
nil,
|
||||
)
|
||||
}
|
||||
|
||||
// Reject absolute paths
|
||||
if filepath.IsAbs(trimmed) {
|
||||
return gibidiutils.NewStructuredError(
|
||||
gibidiutils.ErrorTypeValidation,
|
||||
gibidiutils.CodeValidationPath,
|
||||
"absolute paths are not allowed",
|
||||
trimmed,
|
||||
map[string]any{"path": trimmed},
|
||||
)
|
||||
}
|
||||
|
||||
// Clean and validate path components
|
||||
cleaned := filepath.Clean(trimmed)
|
||||
if filepath.IsAbs(cleaned) || strings.HasPrefix(cleaned, "/") {
|
||||
return gibidiutils.NewStructuredError(
|
||||
gibidiutils.ErrorTypeValidation,
|
||||
gibidiutils.CodeValidationPath,
|
||||
"path must be relative",
|
||||
trimmed,
|
||||
map[string]any{"path": trimmed, "cleaned": cleaned},
|
||||
)
|
||||
}
|
||||
|
||||
// Check for path traversal in components
|
||||
components := strings.Split(filepath.ToSlash(cleaned), "/")
|
||||
for _, component := range components {
|
||||
if component == ".." {
|
||||
return gibidiutils.NewStructuredError(
|
||||
gibidiutils.ErrorTypeValidation,
|
||||
gibidiutils.CodeValidationPath,
|
||||
"path traversal not allowed",
|
||||
trimmed,
|
||||
map[string]any{"path": trimmed, "cleaned": cleaned},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeStreaming writes a large file in streaming chunks.
|
||||
func (w *MarkdownWriter) writeStreaming(req WriteRequest) error {
|
||||
defer w.closeReader(req.Reader, req.Path)
|
||||
// Validate path before use
|
||||
if err := validateMarkdownPath(req.Path); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check for nil reader
|
||||
if req.Reader == nil {
|
||||
return gibidiutils.NewStructuredError(
|
||||
gibidiutils.ErrorTypeValidation,
|
||||
gibidiutils.CodeValidationRequired,
|
||||
"nil reader in write request",
|
||||
"",
|
||||
nil,
|
||||
).WithFilePath(req.Path)
|
||||
}
|
||||
|
||||
defer gibidiutils.SafeCloseReader(req.Reader, req.Path)
|
||||
|
||||
language := detectLanguage(req.Path)
|
||||
|
||||
// Write file header
|
||||
if _, err := fmt.Fprintf(w.outFile, "## File: `%s`\n```%s\n", req.Path, language); err != nil {
|
||||
return utils.WrapError(err, utils.ErrorTypeIO, utils.CodeIOWrite, "failed to write file header").WithFilePath(req.Path)
|
||||
safePath := gibidiutils.EscapeForMarkdown(req.Path)
|
||||
if _, err := fmt.Fprintf(w.outFile, "## File: `%s`\n```%s\n", safePath, language); err != nil {
|
||||
return gibidiutils.WrapError(
|
||||
err, gibidiutils.ErrorTypeIO, gibidiutils.CodeIOWrite,
|
||||
"failed to write file header",
|
||||
).WithFilePath(req.Path)
|
||||
}
|
||||
|
||||
// Stream file content in chunks
|
||||
@@ -64,7 +149,10 @@ func (w *MarkdownWriter) writeStreaming(req WriteRequest) error {
|
||||
|
||||
// Write file footer
|
||||
if _, err := w.outFile.WriteString("\n```\n\n"); err != nil {
|
||||
return utils.WrapError(err, utils.ErrorTypeIO, utils.CodeIOWrite, "failed to write file footer").WithFilePath(req.Path)
|
||||
return gibidiutils.WrapError(
|
||||
err, gibidiutils.ErrorTypeIO, gibidiutils.CodeIOWrite,
|
||||
"failed to write file footer",
|
||||
).WithFilePath(req.Path)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -72,68 +160,55 @@ func (w *MarkdownWriter) writeStreaming(req WriteRequest) error {
|
||||
|
||||
// writeInline writes a small file directly from content.
|
||||
func (w *MarkdownWriter) writeInline(req WriteRequest) error {
|
||||
// Validate path before use
|
||||
if err := validateMarkdownPath(req.Path); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
language := detectLanguage(req.Path)
|
||||
formatted := fmt.Sprintf("## File: `%s`\n```%s\n%s\n```\n\n", req.Path, language, req.Content)
|
||||
safePath := gibidiutils.EscapeForMarkdown(req.Path)
|
||||
formatted := fmt.Sprintf("## File: `%s`\n```%s\n%s\n```\n\n", safePath, language, req.Content)
|
||||
|
||||
if _, err := w.outFile.WriteString(formatted); err != nil {
|
||||
return utils.WrapError(err, utils.ErrorTypeIO, utils.CodeIOWrite, "failed to write inline content").WithFilePath(req.Path)
|
||||
return gibidiutils.WrapError(
|
||||
err, gibidiutils.ErrorTypeIO, gibidiutils.CodeIOWrite,
|
||||
"failed to write inline content",
|
||||
).WithFilePath(req.Path)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// streamContent streams file content in chunks.
|
||||
func (w *MarkdownWriter) streamContent(reader io.Reader, path string) error {
|
||||
buf := make([]byte, StreamChunkSize)
|
||||
for {
|
||||
n, err := reader.Read(buf)
|
||||
if n > 0 {
|
||||
if _, writeErr := w.outFile.Write(buf[:n]); writeErr != nil {
|
||||
return utils.WrapError(writeErr, utils.ErrorTypeIO, utils.CodeIOWrite, "failed to write chunk").WithFilePath(path)
|
||||
}
|
||||
}
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return utils.WrapError(err, utils.ErrorTypeIO, utils.CodeIORead, "failed to read chunk").WithFilePath(path)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return gibidiutils.StreamContent(reader, w.outFile, StreamChunkSize, path, nil)
|
||||
}
|
||||
|
||||
// closeReader safely closes a reader if it implements io.Closer.
|
||||
func (w *MarkdownWriter) closeReader(reader io.Reader, path string) {
|
||||
if closer, ok := reader.(io.Closer); ok {
|
||||
if err := closer.Close(); err != nil {
|
||||
utils.LogError(
|
||||
"Failed to close file reader",
|
||||
utils.WrapError(err, utils.ErrorTypeIO, utils.CodeIOClose, "failed to close file reader").WithFilePath(path),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// startMarkdownWriter handles markdown format output with streaming support.
|
||||
func startMarkdownWriter(outFile *os.File, writeCh <-chan WriteRequest, done chan<- struct{}, prefix, suffix string) {
|
||||
// startMarkdownWriter handles Markdown format output with streaming support.
|
||||
func startMarkdownWriter(
|
||||
outFile *os.File,
|
||||
writeCh <-chan WriteRequest,
|
||||
done chan<- struct{},
|
||||
prefix, suffix string,
|
||||
) {
|
||||
defer close(done)
|
||||
|
||||
writer := NewMarkdownWriter(outFile)
|
||||
|
||||
// Start writing
|
||||
if err := writer.Start(prefix, suffix); err != nil {
|
||||
utils.LogError("Failed to write markdown prefix", err)
|
||||
gibidiutils.LogError("Failed to write markdown prefix", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Process files
|
||||
for req := range writeCh {
|
||||
if err := writer.WriteFile(req); err != nil {
|
||||
utils.LogError("Failed to write markdown file", err)
|
||||
gibidiutils.LogError("Failed to write markdown file", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Close writer
|
||||
if err := writer.Close(suffix); err != nil {
|
||||
utils.LogError("Failed to write markdown suffix", err)
|
||||
gibidiutils.LogError("Failed to write markdown suffix", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user