1
+ use std:: path:: Path ;
1
2
use std:: path:: PathBuf ;
2
3
use tempfile:: Builder ;
3
4
@@ -24,12 +25,16 @@ impl std::error::Error for PasteImageError {}
24
25
#[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
25
26
pub enum EncodedImageFormat {
26
27
Png ,
28
+ Jpeg ,
29
+ Other ,
27
30
}
28
31
29
32
impl EncodedImageFormat {
30
33
pub fn label ( self ) -> & ' static str {
31
34
match self {
32
35
EncodedImageFormat :: Png => "PNG" ,
36
+ EncodedImageFormat :: Jpeg => "JPEG" ,
37
+ EncodedImageFormat :: Other => "IMG" ,
33
38
}
34
39
}
35
40
}
@@ -95,3 +100,185 @@ pub fn paste_image_to_temp_png() -> Result<(PathBuf, PastedImageInfo), PasteImag
95
100
. map_err ( |e| PasteImageError :: IoError ( e. error . to_string ( ) ) ) ?;
96
101
Ok ( ( path, info) )
97
102
}
103
+
104
+ /// Normalize pasted text that may represent a filesystem path.
105
+ ///
106
+ /// Supports:
107
+ /// - `file://` URLs (converted to local paths)
108
+ /// - Windows/UNC paths
109
+ /// - shell-escaped single paths (via `shlex`)
110
+ pub fn normalize_pasted_path ( pasted : & str ) -> Option < PathBuf > {
111
+ let pasted = pasted. trim ( ) ;
112
+
113
+ // file:// URL → filesystem path
114
+ if let Ok ( url) = url:: Url :: parse ( pasted)
115
+ && url. scheme ( ) == "file"
116
+ {
117
+ return url. to_file_path ( ) . ok ( ) ;
118
+ }
119
+
120
+ // TODO: We'll improve the implementation/unit tests over time, as appropriate.
121
+ // Possibly use typed-path: https://github.com/openai/codex/pull/2567/commits/3cc92b78e0a1f94e857cf4674d3a9db918ed352e
122
+ //
123
+ // Detect unquoted Windows paths and bypass POSIX shlex which
124
+ // treats backslashes as escapes (e.g., C:\Users\Alice\file.png).
125
+ // Also handles UNC paths (\\server\share\path).
126
+ let looks_like_windows_path = {
127
+ // Drive letter path: C:\ or C:/
128
+ let drive = pasted
129
+ . chars ( )
130
+ . next ( )
131
+ . map ( |c| c. is_ascii_alphabetic ( ) )
132
+ . unwrap_or ( false )
133
+ && pasted. get ( 1 ..2 ) == Some ( ":" )
134
+ && pasted
135
+ . get ( 2 ..3 )
136
+ . map ( |s| s == "\\ " || s == "/" )
137
+ . unwrap_or ( false ) ;
138
+ // UNC path: \\server\share
139
+ let unc = pasted. starts_with ( "\\ \\ " ) ;
140
+ drive || unc
141
+ } ;
142
+ if looks_like_windows_path {
143
+ return Some ( PathBuf :: from ( pasted) ) ;
144
+ }
145
+
146
+ // shell-escaped single path → unescaped
147
+ let parts: Vec < String > = shlex:: Shlex :: new ( pasted) . collect ( ) ;
148
+ if parts. len ( ) == 1 {
149
+ return parts. into_iter ( ) . next ( ) . map ( PathBuf :: from) ;
150
+ }
151
+
152
+ None
153
+ }
154
+
155
+ /// Infer an image format for the provided path based on its extension.
156
+ pub fn pasted_image_format ( path : & Path ) -> EncodedImageFormat {
157
+ match path
158
+ . extension ( )
159
+ . and_then ( |e| e. to_str ( ) )
160
+ . map ( |s| s. to_ascii_lowercase ( ) )
161
+ . as_deref ( )
162
+ {
163
+ Some ( "png" ) => EncodedImageFormat :: Png ,
164
+ Some ( "jpg" ) | Some ( "jpeg" ) => EncodedImageFormat :: Jpeg ,
165
+ _ => EncodedImageFormat :: Other ,
166
+ }
167
+ }
168
+
169
+ #[ cfg( test) ]
170
+ mod pasted_paths_tests {
171
+ use super :: * ;
172
+
173
+ #[ cfg( not( windows) ) ]
174
+ #[ test]
175
+ fn normalize_file_url ( ) {
176
+ let input = "file:///tmp/example.png" ;
177
+ let result = normalize_pasted_path ( input) . expect ( "should parse file URL" ) ;
178
+ assert_eq ! ( result, PathBuf :: from( "/tmp/example.png" ) ) ;
179
+ }
180
+
181
+ #[ test]
182
+ fn normalize_file_url_windows ( ) {
183
+ let input = r"C:\Temp\example.png" ;
184
+ let result = normalize_pasted_path ( input) . expect ( "should parse file URL" ) ;
185
+ assert_eq ! ( result, PathBuf :: from( r"C:\Temp\example.png" ) ) ;
186
+ }
187
+
188
+ #[ test]
189
+ fn normalize_shell_escaped_single_path ( ) {
190
+ let input = "/home/user/My\\ File.png" ;
191
+ let result = normalize_pasted_path ( input) . expect ( "should unescape shell-escaped path" ) ;
192
+ assert_eq ! ( result, PathBuf :: from( "/home/user/My File.png" ) ) ;
193
+ }
194
+
195
+ #[ test]
196
+ fn normalize_simple_quoted_path_fallback ( ) {
197
+ let input = "\" /home/user/My File.png\" " ;
198
+ let result = normalize_pasted_path ( input) . expect ( "should trim simple quotes" ) ;
199
+ assert_eq ! ( result, PathBuf :: from( "/home/user/My File.png" ) ) ;
200
+ }
201
+
202
+ #[ test]
203
+ fn normalize_single_quoted_unix_path ( ) {
204
+ let input = "'/home/user/My File.png'" ;
205
+ let result = normalize_pasted_path ( input) . expect ( "should trim single quotes via shlex" ) ;
206
+ assert_eq ! ( result, PathBuf :: from( "/home/user/My File.png" ) ) ;
207
+ }
208
+
209
+ #[ test]
210
+ fn normalize_multiple_tokens_returns_none ( ) {
211
+ // Two tokens after shell splitting → not a single path
212
+ let input = "/home/user/a\\ b.png /home/user/c.png" ;
213
+ let result = normalize_pasted_path ( input) ;
214
+ assert ! ( result. is_none( ) ) ;
215
+ }
216
+
217
+ #[ test]
218
+ fn pasted_image_format_png_jpeg_unknown ( ) {
219
+ assert_eq ! (
220
+ pasted_image_format( Path :: new( "/a/b/c.PNG" ) ) ,
221
+ EncodedImageFormat :: Png
222
+ ) ;
223
+ assert_eq ! (
224
+ pasted_image_format( Path :: new( "/a/b/c.jpg" ) ) ,
225
+ EncodedImageFormat :: Jpeg
226
+ ) ;
227
+ assert_eq ! (
228
+ pasted_image_format( Path :: new( "/a/b/c.JPEG" ) ) ,
229
+ EncodedImageFormat :: Jpeg
230
+ ) ;
231
+ assert_eq ! (
232
+ pasted_image_format( Path :: new( "/a/b/c" ) ) ,
233
+ EncodedImageFormat :: Other
234
+ ) ;
235
+ assert_eq ! (
236
+ pasted_image_format( Path :: new( "/a/b/c.webp" ) ) ,
237
+ EncodedImageFormat :: Other
238
+ ) ;
239
+ }
240
+
241
+ #[ test]
242
+ fn normalize_single_quoted_windows_path ( ) {
243
+ let input = r"'C:\\Users\\Alice\\My File.jpeg'" ;
244
+ let result =
245
+ normalize_pasted_path ( input) . expect ( "should trim single quotes on windows path" ) ;
246
+ assert_eq ! ( result, PathBuf :: from( r"C:\\Users\\Alice\\My File.jpeg" ) ) ;
247
+ }
248
+
249
+ #[ test]
250
+ fn normalize_unquoted_windows_path_with_spaces ( ) {
251
+ let input = r"C:\\Users\\Alice\\My Pictures\\example image.png" ;
252
+ let result = normalize_pasted_path ( input) . expect ( "should accept unquoted windows path" ) ;
253
+ assert_eq ! (
254
+ result,
255
+ PathBuf :: from( r"C:\\Users\\Alice\\My Pictures\\example image.png" )
256
+ ) ;
257
+ }
258
+
259
+ #[ test]
260
+ fn normalize_unc_windows_path ( ) {
261
+ let input = r"\\\\server\\share\\folder\\file.jpg" ;
262
+ let result = normalize_pasted_path ( input) . expect ( "should accept UNC windows path" ) ;
263
+ assert_eq ! (
264
+ result,
265
+ PathBuf :: from( r"\\\\server\\share\\folder\\file.jpg" )
266
+ ) ;
267
+ }
268
+
269
+ #[ test]
270
+ fn pasted_image_format_with_windows_style_paths ( ) {
271
+ assert_eq ! (
272
+ pasted_image_format( Path :: new( r"C:\\a\\b\\c.PNG" ) ) ,
273
+ EncodedImageFormat :: Png
274
+ ) ;
275
+ assert_eq ! (
276
+ pasted_image_format( Path :: new( r"C:\\a\\b\\c.jpeg" ) ) ,
277
+ EncodedImageFormat :: Jpeg
278
+ ) ;
279
+ assert_eq ! (
280
+ pasted_image_format( Path :: new( r"C:\\a\\b\\noext" ) ) ,
281
+ EncodedImageFormat :: Other
282
+ ) ;
283
+ }
284
+ }
0 commit comments