mirror of
https://github.com/ivuorinen/gibidify.git
synced 2026-01-26 11:34:03 +00:00
* 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
215 lines
5.4 KiB
Go
215 lines
5.4 KiB
Go
package fileproc
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/ivuorinen/gibidify/gibidiutils"
|
|
)
|
|
|
|
// MarkdownWriter handles Markdown format output with streaming support.
|
|
type MarkdownWriter struct {
|
|
outFile *os.File
|
|
}
|
|
|
|
// NewMarkdownWriter creates a new markdown writer.
|
|
func NewMarkdownWriter(outFile *os.File) *MarkdownWriter {
|
|
return &MarkdownWriter{outFile: outFile}
|
|
}
|
|
|
|
// Start writes the markdown header.
|
|
func (w *MarkdownWriter) Start(prefix, _ string) error {
|
|
if prefix != "" {
|
|
if _, err := fmt.Fprintf(w.outFile, "# %s\n\n", prefix); err != nil {
|
|
return gibidiutils.WrapError(
|
|
err,
|
|
gibidiutils.ErrorTypeIO,
|
|
gibidiutils.CodeIOWrite,
|
|
"failed to write prefix",
|
|
)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// WriteFile writes a file entry in Markdown format.
|
|
func (w *MarkdownWriter) WriteFile(req WriteRequest) error {
|
|
if req.IsStream {
|
|
return w.writeStreaming(req)
|
|
}
|
|
return w.writeInline(req)
|
|
}
|
|
|
|
// Close writes the markdown footer.
|
|
func (w *MarkdownWriter) Close(suffix string) error {
|
|
if suffix != "" {
|
|
if _, err := fmt.Fprintf(w.outFile, "\n# %s\n", suffix); err != nil {
|
|
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 {
|
|
// 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
|
|
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
|
|
if err := w.streamContent(req.Reader, req.Path); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Write file footer
|
|
if _, err := w.outFile.WriteString("\n```\n\n"); err != nil {
|
|
return gibidiutils.WrapError(
|
|
err, gibidiutils.ErrorTypeIO, gibidiutils.CodeIOWrite,
|
|
"failed to write file footer",
|
|
).WithFilePath(req.Path)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// 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)
|
|
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 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 {
|
|
return gibidiutils.StreamContent(reader, w.outFile, StreamChunkSize, path, nil)
|
|
}
|
|
|
|
// 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 {
|
|
gibidiutils.LogError("Failed to write markdown prefix", err)
|
|
return
|
|
}
|
|
|
|
// Process files
|
|
for req := range writeCh {
|
|
if err := writer.WriteFile(req); err != nil {
|
|
gibidiutils.LogError("Failed to write markdown file", err)
|
|
}
|
|
}
|
|
|
|
// Close writer
|
|
if err := writer.Close(suffix); err != nil {
|
|
gibidiutils.LogError("Failed to write markdown suffix", err)
|
|
}
|
|
}
|