GemReader/cmd/gemreader/main.go

148 lines
4 KiB
Go
Raw Permalink Normal View History

2025-09-29 22:45:20 -05:00
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
"gemreader/internal/config"
"gemreader/internal/tui"
"github.com/charmbracelet/bubbletea"
"github.com/spf13/cobra"
)
const (
MaxFileSize = 10 * 1024 * 1024 // 10MB limit
)
// validateFilePath ensures the file path is safe by resolving it relative to the current working directory
func validateFilePath(inputPath string) (string, error) {
// Clean the path to remove any .. elements
cleanPath := filepath.Clean(inputPath)
// Get the current working directory
currentDir, err := os.Getwd()
if err != nil {
return "", err
}
// Join the current working directory with the clean path
absPath := filepath.Join(currentDir, cleanPath)
// Resolve the absolute path to remove any .. elements
resolvedPath, err := filepath.Abs(absPath)
if err != nil {
return "", err
}
// Ensure the resolved path is within the current directory
relPath, err := filepath.Rel(currentDir, resolvedPath)
if err != nil {
return "", err
}
// If the relative path starts with .. or is an absolute path, it's trying to go outside the current directory
if strings.HasPrefix(relPath, "..") || filepath.IsAbs(relPath) {
return "", fmt.Errorf("path traversal detected: %s", inputPath)
}
return resolvedPath, nil
}
// validateFile performs additional security checks on the file
func validateFile(filePath string) error {
// Check file size to prevent loading extremely large files
fileInfo, err := os.Stat(filePath)
if err != nil {
return err
}
if fileInfo.Size() > MaxFileSize {
return fmt.Errorf("file size too large: %d bytes (max: %d bytes)", fileInfo.Size(), MaxFileSize)
}
// Check file extension to ensure it's a text/markdown file
ext := strings.ToLower(filepath.Ext(filePath))
if ext != ".md" && ext != ".markdown" && ext != ".txt" && ext != "" {
// If there's no extension, we'll allow it since some markdown files don't have extensions
// If there's an extension, it should be markdown-related or text
if ext != "" {
return fmt.Errorf("unsupported file type: %s (only .md, .markdown, .txt files are allowed)", ext)
}
}
return nil
}
var rootCmd = &cobra.Command{
Use: "gemreader [file]",
Short: "A markdown viewer for your terminal.",
Args: cobra.MaximumNArgs(1), // Allow 0 or 1 arguments
Run: func(cmd *cobra.Command, args []string) {
// Load configuration
cfg, err := config.LoadConfig()
if err != nil {
fmt.Printf("could not load config: %v\n", err)
// Continue with default behavior if config loading fails
cfg = config.Config{Title: "GemReader", DefaultFile: ""} // default values
}
var filePath string
if len(args) > 0 {
// Use the provided file path from command line arguments
filePath = args[0]
// Validate the file path to prevent directory traversal attacks
securePath, err := validateFilePath(args[0])
if err != nil {
fmt.Printf("invalid file path: %v\n", err)
os.Exit(1)
}
filePath = securePath
} else if cfg.DefaultFile != "" {
// Use the default file from configuration
securePath, err := validateFilePath(cfg.DefaultFile)
if err != nil {
fmt.Printf("invalid default file path in config: %v\n", err)
os.Exit(1)
}
filePath = securePath
} else {
// No file provided and no default file configured
fmt.Println("No file provided and no default file configured. Please provide a file or set default_file in config.toml")
os.Exit(1)
}
// Perform additional security validation on the file
if err := validateFile(filePath); err != nil {
fmt.Printf("file validation failed: %v\n", err)
os.Exit(1)
}
content, err := os.ReadFile(filePath)
if err != nil {
fmt.Printf("could not read file: %v\n", err)
os.Exit(1)
}
// Pass raw content to TUI so it can generate TOC from the original markdown
m := tui.NewModel(string(content), cfg)
p := tea.NewProgram(m)
if err := p.Start(); err != nil {
fmt.Printf("Alas, there's been an error: %v\n", err)
os.Exit(1)
}
},
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func main() {
Execute()
}