77"""
88
99import os
10+ import re
1011import shutil
1112import subprocess
1213from typing import Optional
1314
1415
16+ def _script_fu_escape (s : str ) -> str :
17+ """Escape a string for safe embedding in Script-Fu double-quoted strings.
18+
19+ Escapes backslash, double-quote, and control characters to prevent
20+ Script-Fu injection through path literals.
21+ """
22+ # Escape backslash first, then double quotes, then other special chars
23+ s = s .replace ('\\ ' , '\\ \\ ' )
24+ s = s .replace ('"' , '\\ "' )
25+ # Remove or escape control characters (newline, tab, etc.)
26+ s = re .sub (r'[\x00-\x1f\x7f]' , lambda m : f'\\ x{ ord (m .group ()):02x} ' , s )
27+ return s
28+
29+
1530def find_gimp () -> str :
1631 """Find the GIMP executable. Raises RuntimeError if not found."""
1732 for name in ("gimp" , "gimp-2.10" , "gimp-2.99" ):
@@ -73,6 +88,7 @@ def create_and_export(
7388) -> dict :
7489 """Create a new image in GIMP and export it."""
7590 abs_output = os .path .abspath (output_path )
91+ safe_abs_output = _script_fu_escape (abs_output )
7692 os .makedirs (os .path .dirname (abs_output ), exist_ok = True )
7793
7894 ext = os .path .splitext (output_path )[1 ].lower ()
@@ -81,22 +97,22 @@ def create_and_export(
8197 if ext == ".png" :
8298 export_cmd = (
8399 f'(file-png-save RUN-NONINTERACTIVE image layer '
84- f'"{ abs_output } " "{ abs_output } " 0 9 1 1 1 1 1)'
100+ f'"{ safe_abs_output } " "{ safe_abs_output } " 0 9 1 1 1 1 1)'
85101 )
86102 elif ext in (".jpg" , ".jpeg" ):
87103 export_cmd = (
88104 f'(file-jpeg-save RUN-NONINTERACTIVE image layer '
89- f'"{ abs_output } " "{ abs_output } " 0.85 0.0 0 0 "" 0 1 0 2)'
105+ f'"{ safe_abs_output } " "{ safe_abs_output } " 0.85 0.0 0 0 "" 0 1 0 2)'
90106 )
91107 elif ext == ".bmp" :
92108 export_cmd = (
93109 f'(file-bmp-save RUN-NONINTERACTIVE image layer '
94- f'"{ abs_output } " "{ abs_output } " 0)'
110+ f'"{ safe_abs_output } " "{ safe_abs_output } " 0)'
95111 )
96112 else :
97113 export_cmd = (
98114 f'(gimp-file-overwrite RUN-NONINTERACTIVE image layer '
99- f'"{ abs_output } " "{ abs_output } ")'
115+ f'"{ safe_abs_output } " "{ safe_abs_output } ")'
100116 )
101117
102118 # Color mapping
@@ -162,28 +178,30 @@ def apply_filter_and_export(
162178
163179 abs_input = os .path .abspath (input_path )
164180 abs_output = os .path .abspath (output_path )
181+ safe_abs_input = _script_fu_escape (abs_input )
182+ safe_abs_output = _script_fu_escape (abs_output )
165183 os .makedirs (os .path .dirname (abs_output ), exist_ok = True )
166184
167185 ext = os .path .splitext (output_path )[1 ].lower ()
168186 if ext == ".png" :
169187 export_cmd = (
170188 f'(file-png-save RUN-NONINTERACTIVE image drawable '
171- f'"{ abs_output } " "{ abs_output } " 0 9 1 1 1 1 1)'
189+ f'"{ safe_abs_output } " "{ safe_abs_output } " 0 9 1 1 1 1 1)'
172190 )
173191 elif ext in (".jpg" , ".jpeg" ):
174192 export_cmd = (
175193 f'(file-jpeg-save RUN-NONINTERACTIVE image drawable '
176- f'"{ abs_output } " "{ abs_output } " 0.85 0.0 0 0 "" 0 1 0 2)'
194+ f'"{ safe_abs_output } " "{ safe_abs_output } " 0.85 0.0 0 0 "" 0 1 0 2)'
177195 )
178196 else :
179197 export_cmd = (
180198 f'(gimp-file-overwrite RUN-NONINTERACTIVE image drawable '
181- f'"{ abs_output } " "{ abs_output } ")'
199+ f'"{ safe_abs_output } " "{ safe_abs_output } ")'
182200 )
183201
184202 script = (
185203 f'(let* ('
186- f'(image (car (file-png-load RUN-NONINTERACTIVE "{ abs_input } " "{ abs_input } ")))'
204+ f'(image (car (file-png-load RUN-NONINTERACTIVE "{ safe_abs_input } " "{ safe_abs_input } ")))'
187205 f'(drawable (car (gimp-image-flatten image)))'
188206 f')'
189207 f'{ script_fu_filter } '
0 commit comments