@@ -95,9 +95,9 @@ func encodeImageToDataURL(filePath string) (string, error) {
9595 }
9696
9797 contentType := http .DetectContentType (buf )
98- allowedTypes := [] string { "image/jpeg" , "image/png" , " image/webp" }
99- if ! slices . Contains ( allowedTypes , contentType ) {
100- return "" , fmt .Errorf ("invalid image type: %s" , contentType )
98+ // Validate that this is an image file using both content and extension
99+ if ! isImageFileByContentAndExtension ( buf , filePath ) {
100+ return "" , fmt .Errorf ("invalid image type for file : %s" , filePath )
101101 }
102102
103103 info , err := file .Stat ()
@@ -158,3 +158,113 @@ func processImagesInPrompt(prompt string) (string, []string, error) {
158158
159159 return strings .TrimSpace (prompt ), imageDataURLs , nil
160160}
161+
162+ // extractFileInclusions finds file paths in the prompt text using the @ symbol
163+ // e.g., @filename.txt, @./path/to/file.txt, @/absolute/path/file.txt
164+ func extractFileInclusions (prompt string ) []string {
165+ // Regex to match @ followed by a file path
166+ // Pattern explanation:
167+ // - @ symbol before the path
168+ // - File path can be quoted (at least one char) or unquoted (at least one char)
169+ // - Supports relative (./, ../) and absolute paths
170+ regexPattern := `@(?:"([^"]+)"|'([^']+)'|([^\s"']+))`
171+ re := regexp .MustCompile (regexPattern )
172+ matches := re .FindAllStringSubmatch (prompt , - 1 )
173+
174+ paths := []string {}
175+ for _ , match := range matches {
176+ // match[0] is the full match
177+ // match[1] is double-quoted content
178+ // match[2] is single-quoted content
179+ // match[3] is unquoted content
180+ if len (match ) >= 2 && match [1 ] != "" {
181+ paths = append (paths , match [1 ])
182+ } else if len (match ) >= 3 && match [2 ] != "" {
183+ paths = append (paths , match [2 ])
184+ } else if len (match ) >= 4 && match [3 ] != "" {
185+ paths = append (paths , match [3 ])
186+ }
187+ }
188+
189+ return paths
190+ }
191+
192+ // hasValidImageExtension checks if the file has a valid image extension
193+ func hasValidImageExtension (filePath string ) bool {
194+ filePathLower := strings .ToLower (filePath )
195+ return strings .HasSuffix (filePathLower , ".jpg" ) ||
196+ strings .HasSuffix (filePathLower , ".jpeg" ) ||
197+ strings .HasSuffix (filePathLower , ".png" ) ||
198+ strings .HasSuffix (filePathLower , ".webp" )
199+ }
200+
201+ // isImageFileByContentAndExtension checks if a file is an image using both file extension and content type detection
202+ func isImageFileByContentAndExtension (content []byte , filePath string ) bool {
203+ // First check content type
204+ contentType := http .DetectContentType (content )
205+ allowedTypes := []string {"image/jpeg" , "image/jpg" , "image/png" , "image/webp" }
206+ if slices .Contains (allowedTypes , contentType ) {
207+ return true
208+ }
209+
210+ // Fallback to file extension check if content detection fails
211+ return hasValidImageExtension (filePath )
212+ }
213+
214+ // processFileInclusions extracts files mentioned with @ symbol, reads their contents,
215+ // and returns the prompt with file contents embedded
216+ func processFileInclusions (prompt string ) (string , error ) {
217+ filePaths := extractFileInclusions (prompt )
218+
219+ // Process each file inclusion in order
220+ for _ , filePath := range filePaths {
221+ nfp := normalizeFilePath (filePath )
222+
223+ // Read the file content
224+ content , err := os .ReadFile (nfp )
225+ if err != nil {
226+ // Skip non-existent files or files that can't be read
227+ continue
228+ }
229+
230+ // Check if the file is an image to handle it appropriately
231+ // Only content is checked here, not file extension, to maintain original behavior
232+ if isImageFileByContentAndExtension (content , nfp ) {
233+ // For image files, we keep the original file reference (@filePath) in the prompt
234+ // so that processImagesInPrompt can handle it properly
235+ continue
236+ }
237+
238+ // For non-image files, replace the @filename with the file content
239+ // Try different variations to match how it appears in the prompt
240+ escapedPath := regexp .QuoteMeta (filePath ) // Escape special regex chars
241+ quotedPath := regexp .QuoteMeta (nfp ) // Also try normalized path
242+
243+ // Replace all occurrences of the file reference with its content
244+ contentStr := string (content )
245+
246+ // Replace @ symbol usage with the file content - preserve the space or end by using word boundaries
247+ // Use capturing groups to preserve the space
248+ prompt = regexp .MustCompile (`@` + `"` + escapedPath + `"` ).ReplaceAllString (prompt , contentStr )
249+ prompt = regexp .MustCompile (`@` + `'` + escapedPath + `'` ).ReplaceAllString (prompt , contentStr )
250+ // For the unquoted version, we need to match @ + path and replace with content + the boundary character
251+ // Use a more specific pattern that captures the space or end
252+ prompt = regexp .MustCompile (`(@` + escapedPath + `)(\s|$)` ).ReplaceAllString (prompt , contentStr + "$2" )
253+
254+ // Also try replacing with normalized path
255+ prompt = regexp .MustCompile (`@` + `"` + quotedPath + `"` ).ReplaceAllString (prompt , contentStr )
256+ prompt = regexp .MustCompile (`@` + `'` + quotedPath + `'` ).ReplaceAllString (prompt , contentStr )
257+ prompt = regexp .MustCompile (`(@` + quotedPath + `)(\s|$)` ).ReplaceAllString (prompt , contentStr + "$2" )
258+ }
259+
260+ return prompt , nil
261+ }
262+
263+ // For testing purposes - making the functions public so they can be tested
264+ func ExtractFileInclusions (prompt string ) []string {
265+ return extractFileInclusions (prompt )
266+ }
267+
268+ func ProcessFileInclusions (prompt string ) (string , error ) {
269+ return processFileInclusions (prompt )
270+ }
0 commit comments