Skip to content

Commit 21aa6fb

Browse files
committed
feat: handle escaped characters in image paths
1 parent ab7e271 commit 21aa6fb

File tree

2 files changed

+79
-5
lines changed

2 files changed

+79
-5
lines changed

npm-app/src/__tests__/image-upload.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,5 +253,33 @@ describe('Image Upload Functionality', () => {
253253
expect(result.success).toBe(false)
254254
expect(result.error).toContain('Unsupported image format')
255255
})
256+
257+
test('should normalize unicode escape sequences in provided paths', async () => {
258+
const actualFilename = 'Screenshot 2025-09-29 at 4.09.19 PM.png'
259+
const filePath = path.join(TEST_DIR, actualFilename)
260+
writeFileSync(filePath, MINIMAL_PNG)
261+
262+
const variations = [
263+
'Screenshot 2025-09-29 at 4.09.19\\u{202f}PM.png',
264+
'Screenshot 2025-09-29 at 4.09.19\\u202fPM.png',
265+
]
266+
267+
for (const candidate of variations) {
268+
const result = await processImageFile(candidate, TEST_DIR)
269+
expect(result.success).toBe(true)
270+
expect(result.imagePart?.filename).toBe(actualFilename)
271+
}
272+
})
273+
274+
test('should handle shell-escaped characters in paths', async () => {
275+
const spacedFilename = 'My Screenshot (Final).png'
276+
const filePath = path.join(TEST_DIR, spacedFilename)
277+
writeFileSync(filePath, MINIMAL_PNG)
278+
279+
const result = await processImageFile('My\\ Screenshot\\ \(Final\).png', TEST_DIR)
280+
281+
expect(result.success).toBe(true)
282+
expect(result.imagePart?.filename).toBe(spacedFilename)
283+
})
256284
})
257285
})

npm-app/src/utils/image-handler.ts

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,51 @@ const MAX_BASE64_SIZE = 150 * 1024 // 150KB max for base64 (backend limit ~760KB
3939
const COMPRESSION_QUALITIES = [80, 60, 40, 20] // JPEG quality levels to try
4040
const DIMENSION_LIMITS = [800, 600, 400, 300] // Max dimensions to try
4141

42+
function normalizeUserProvidedPath(filePath: string): string {
43+
let normalized = filePath
44+
45+
normalized = normalized.replace(/\\u\{([0-9a-fA-F]+)\}/g, (match, codePoint) => {
46+
const value = Number.parseInt(codePoint, 16)
47+
if (Number.isNaN(value)) {
48+
return match
49+
}
50+
try {
51+
return String.fromCodePoint(value)
52+
} catch {
53+
return match
54+
}
55+
})
56+
57+
normalized = normalized.replace(/\\u([0-9a-fA-F]{4})/g, (match, codePoint) => {
58+
const value = Number.parseInt(codePoint, 16)
59+
if (Number.isNaN(value)) {
60+
return match
61+
}
62+
try {
63+
return String.fromCodePoint(value)
64+
} catch {
65+
return match
66+
}
67+
})
68+
69+
normalized = normalized.replace(/\\x([0-9a-fA-F]{2})/g, (match, codePoint) => {
70+
const value = Number.parseInt(codePoint, 16)
71+
if (Number.isNaN(value)) {
72+
return match
73+
}
74+
return String.fromCharCode(value)
75+
})
76+
77+
normalized = normalized.replace(/\\([ \t"'(){}\[\]])/g, (match, char) => {
78+
if (char === '\\') {
79+
return '\\'
80+
}
81+
return char
82+
})
83+
84+
return normalized
85+
}
86+
4287
/**
4388
* Detects MIME type from file extension
4489
*/
@@ -77,13 +122,14 @@ export function isImageFile(filePath: string): boolean {
77122
* Resolves a file path, handling ~, relative paths, etc.
78123
*/
79124
export function resolveFilePath(filePath: string, cwd: string): string {
80-
if (filePath.startsWith('~')) {
81-
return path.join(homedir(), filePath.slice(1))
125+
const normalized = normalizeUserProvidedPath(filePath)
126+
if (normalized.startsWith('~')) {
127+
return path.join(homedir(), normalized.slice(1))
82128
}
83-
if (path.isAbsolute(filePath)) {
84-
return filePath
129+
if (path.isAbsolute(normalized)) {
130+
return normalized
85131
}
86-
return path.resolve(cwd, filePath)
132+
return path.resolve(cwd, normalized)
87133
}
88134

89135
/**

0 commit comments

Comments
 (0)