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() }