mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 08:03:22 +00:00
fix(windows): reject multi-file image paste
This commit is contained in:
@@ -176,6 +176,12 @@ function dirent(name: string, directory = true): { name: string; isDirectory: ()
|
||||
return { name, isDirectory: () => directory }
|
||||
}
|
||||
|
||||
function shellIdListArray(childCount: number): Buffer {
|
||||
const value = Buffer.alloc(4 + 4 * (childCount + 1))
|
||||
value.writeUInt32LE(childCount)
|
||||
return value
|
||||
}
|
||||
|
||||
describe('registerClipboardHandlers', () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(Date, 'now').mockReturnValue(1760000000000)
|
||||
@@ -578,7 +584,9 @@ describe('registerClipboardHandlers', () => {
|
||||
const sourcePath = 'C:\\Users\\alice\\图片\\copied-image.png'
|
||||
const png = Buffer.from([4, 3, 2, 1])
|
||||
clipboardReadImageMock.mockReturnValue({ isEmpty: () => true })
|
||||
clipboardReadBufferMock.mockReturnValue(Buffer.from(`${sourcePath}\0`, 'utf16le'))
|
||||
clipboardReadBufferMock.mockImplementation((format: string) =>
|
||||
format === 'FileNameW' ? Buffer.from(`${sourcePath}\0`, 'utf16le') : shellIdListArray(1)
|
||||
)
|
||||
const source = Buffer.alloc(24)
|
||||
Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]).copy(source)
|
||||
source.writeUInt32BE(13, 8)
|
||||
@@ -614,6 +622,7 @@ describe('registerClipboardHandlers', () => {
|
||||
'/var/tmp/orca-paste-1760000000000-00000000-0000-4000-8000-000000000000.png'
|
||||
)
|
||||
expect(clipboardReadBufferMock).toHaveBeenCalledWith('FileNameW')
|
||||
expect(clipboardReadBufferMock).toHaveBeenCalledWith('Shell IDList Array')
|
||||
expect(fsOpenMock).toHaveBeenCalledWith(sourcePath, 'r')
|
||||
expect(nativeImageCreateFromBufferMock).toHaveBeenCalledWith(source)
|
||||
expect(close).toHaveBeenCalled()
|
||||
|
||||
@@ -107,7 +107,10 @@ export function registerClipboardHandlers(store: Store): void {
|
||||
return null
|
||||
}
|
||||
const copiedFilePng = await readWindowsClipboardImageFileAsPng(
|
||||
clipboard.readBuffer('FileNameW'),
|
||||
{
|
||||
fileNameW: clipboard.readBuffer('FileNameW'),
|
||||
shellIdListArray: clipboard.readBuffer('Shell IDList Array')
|
||||
},
|
||||
{
|
||||
createImageFromBuffer: (buffer) => nativeImage.createFromBuffer(buffer),
|
||||
openFile: (filePath) => open(filePath, 'r')
|
||||
|
||||
@@ -9,6 +9,12 @@ function fileNameW(filePath: string): Buffer {
|
||||
return Buffer.from(`${filePath}\0`, 'utf16le')
|
||||
}
|
||||
|
||||
function clipboardFormats(filePath: string, shellItemCount = 1) {
|
||||
const shellIdListArray = Buffer.alloc(4 + 4 * (shellItemCount + 1))
|
||||
shellIdListArray.writeUInt32LE(shellItemCount)
|
||||
return { fileNameW: fileNameW(filePath), shellIdListArray }
|
||||
}
|
||||
|
||||
function pngHeader(width = 10, height = 10): Buffer {
|
||||
const source = Buffer.alloc(24)
|
||||
Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]).copy(source)
|
||||
@@ -85,7 +91,7 @@ describe('readWindowsClipboardImageFileAsPng', () => {
|
||||
const createImageFromBuffer = vi.fn(() => image(png) as never)
|
||||
|
||||
await expect(
|
||||
readWindowsClipboardImageFileAsPng(fileNameW(filePath), {
|
||||
readWindowsClipboardImageFileAsPng(clipboardFormats(filePath), {
|
||||
createImageFromBuffer,
|
||||
openFile
|
||||
})
|
||||
@@ -115,7 +121,25 @@ describe('readWindowsClipboardImageFileAsPng', () => {
|
||||
const createImageFromBuffer = vi.fn()
|
||||
|
||||
await expect(
|
||||
readWindowsClipboardImageFileAsPng(payload, { createImageFromBuffer, openFile })
|
||||
readWindowsClipboardImageFileAsPng(
|
||||
{ fileNameW: payload, shellIdListArray: Buffer.alloc(0) },
|
||||
{ createImageFromBuffer, openFile }
|
||||
)
|
||||
).resolves.toBeNull()
|
||||
|
||||
expect(openFile).not.toHaveBeenCalled()
|
||||
expect(createImageFromBuffer).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('rejects an Explorer multi-selection when FileNameW exposes only its first path', async () => {
|
||||
const openFile = vi.fn()
|
||||
const createImageFromBuffer = vi.fn()
|
||||
|
||||
await expect(
|
||||
readWindowsClipboardImageFileAsPng(clipboardFormats('C:\\one.png', 2), {
|
||||
createImageFromBuffer,
|
||||
openFile
|
||||
})
|
||||
).resolves.toBeNull()
|
||||
|
||||
expect(openFile).not.toHaveBeenCalled()
|
||||
@@ -128,7 +152,7 @@ describe('readWindowsClipboardImageFileAsPng', () => {
|
||||
const openFile = vi.fn().mockRejectedValue(error)
|
||||
|
||||
await expect(
|
||||
readWindowsClipboardImageFileAsPng(fileNameW('C:\\shot.png'), {
|
||||
readWindowsClipboardImageFileAsPng(clipboardFormats('C:\\shot.png'), {
|
||||
createImageFromBuffer: vi.fn(),
|
||||
openFile
|
||||
})
|
||||
@@ -141,7 +165,7 @@ describe('readWindowsClipboardImageFileAsPng', () => {
|
||||
const createImageFromBuffer = vi.fn()
|
||||
|
||||
await expect(
|
||||
readWindowsClipboardImageFileAsPng(fileNameW('C:\\images.png'), {
|
||||
readWindowsClipboardImageFileAsPng(clipboardFormats('C:\\images.png'), {
|
||||
createImageFromBuffer,
|
||||
openFile: vi.fn().mockResolvedValue(handle)
|
||||
})
|
||||
@@ -159,7 +183,7 @@ describe('readWindowsClipboardImageFileAsPng', () => {
|
||||
const createImageFromBuffer = vi.fn()
|
||||
|
||||
await expect(
|
||||
readWindowsClipboardImageFileAsPng(fileNameW('C:\\huge.png'), {
|
||||
readWindowsClipboardImageFileAsPng(clipboardFormats('C:\\huge.png'), {
|
||||
createImageFromBuffer,
|
||||
openFile: vi.fn().mockResolvedValue(handle)
|
||||
})
|
||||
@@ -175,7 +199,7 @@ describe('readWindowsClipboardImageFileAsPng', () => {
|
||||
const createImageFromBuffer = vi.fn()
|
||||
|
||||
await expect(
|
||||
readWindowsClipboardImageFileAsPng(fileNameW('C:\\changed.png'), {
|
||||
readWindowsClipboardImageFileAsPng(clipboardFormats('C:\\changed.png'), {
|
||||
createImageFromBuffer,
|
||||
openFile: vi.fn().mockResolvedValue(handle)
|
||||
})
|
||||
@@ -189,7 +213,7 @@ describe('readWindowsClipboardImageFileAsPng', () => {
|
||||
const handle = fileHandle(Buffer.from('not-an-image'))
|
||||
|
||||
await expect(
|
||||
readWindowsClipboardImageFileAsPng(fileNameW('C:\\invalid.png'), {
|
||||
readWindowsClipboardImageFileAsPng(clipboardFormats('C:\\invalid.png'), {
|
||||
createImageFromBuffer: vi.fn(() => ({ isEmpty: () => true }) as never),
|
||||
openFile: vi.fn().mockResolvedValue(handle)
|
||||
})
|
||||
@@ -201,7 +225,7 @@ describe('readWindowsClipboardImageFileAsPng', () => {
|
||||
const createImageFromBuffer = vi.fn()
|
||||
|
||||
await expect(
|
||||
readWindowsClipboardImageFileAsPng(fileNameW('C:\\pixel-bomb.png'), {
|
||||
readWindowsClipboardImageFileAsPng(clipboardFormats('C:\\pixel-bomb.png'), {
|
||||
createImageFromBuffer,
|
||||
openFile: vi.fn().mockResolvedValue(handle)
|
||||
})
|
||||
@@ -220,7 +244,7 @@ describe('readWindowsClipboardImageFileAsPng', () => {
|
||||
const createImageFromBuffer = vi.fn()
|
||||
|
||||
await expect(
|
||||
readWindowsClipboardImageFileAsPng(fileNameW('C:\\marker-flood.jpg'), {
|
||||
readWindowsClipboardImageFileAsPng(clipboardFormats('C:\\marker-flood.jpg'), {
|
||||
createImageFromBuffer,
|
||||
openFile: vi.fn().mockResolvedValue(fileHandle(source))
|
||||
})
|
||||
@@ -232,7 +256,7 @@ describe('readWindowsClipboardImageFileAsPng', () => {
|
||||
it('bounds decoded dimensions and converted PNG bytes', async () => {
|
||||
const oversizedDimensions = fileHandle(pngHeader())
|
||||
await expect(
|
||||
readWindowsClipboardImageFileAsPng(fileNameW('C:\\wide.png'), {
|
||||
readWindowsClipboardImageFileAsPng(clipboardFormats('C:\\wide.png'), {
|
||||
createImageFromBuffer: vi.fn(
|
||||
() =>
|
||||
({
|
||||
@@ -247,7 +271,7 @@ describe('readWindowsClipboardImageFileAsPng', () => {
|
||||
|
||||
const oversizedPng = fileHandle(pngHeader())
|
||||
await expect(
|
||||
readWindowsClipboardImageFileAsPng(fileNameW('C:\\expanded.png'), {
|
||||
readWindowsClipboardImageFileAsPng(clipboardFormats('C:\\expanded.png'), {
|
||||
createImageFromBuffer: vi.fn(
|
||||
() => image(Buffer.alloc(CLIPBOARD_IMAGE_MAX_SOURCE_BYTES + 1)) as never
|
||||
),
|
||||
|
||||
@@ -13,6 +13,11 @@ type WindowsClipboardImageFileDeps = {
|
||||
openFile: (filePath: string) => Promise<ClipboardImageFileHandle>
|
||||
}
|
||||
|
||||
type WindowsClipboardImageFileFormats = {
|
||||
fileNameW: Buffer
|
||||
shellIdListArray: Buffer
|
||||
}
|
||||
|
||||
const FILE_NAME_W_MAX_BYTES = 64 * 1024
|
||||
const FILE_READ_MAX_CALLS = 1024
|
||||
const IMAGE_FILE_EXTENSION_SET = new Set(['.jpeg', '.jpg', '.png'])
|
||||
@@ -63,6 +68,14 @@ function decodeFileNameW(value: Buffer): string | null {
|
||||
return IMAGE_FILE_EXTENSION_SET.has(win32.extname(filePath).toLowerCase()) ? filePath : null
|
||||
}
|
||||
|
||||
function hasAtMostOneShellItem(value: Buffer): boolean {
|
||||
if (value.byteLength === 0) {
|
||||
return true
|
||||
}
|
||||
// Why: Explorer's FileNameW exposes only the first path even when its CIDA has multiple items.
|
||||
return value.byteLength >= 12 && value.readUInt32LE(0) === 1
|
||||
}
|
||||
|
||||
function readPngDimensions(source: Buffer): { height: number; width: number } | null {
|
||||
if (
|
||||
source.byteLength < 24 ||
|
||||
@@ -140,9 +153,12 @@ async function readStableFile(
|
||||
}
|
||||
|
||||
export async function readWindowsClipboardImageFileAsPng(
|
||||
fileNameW: Buffer,
|
||||
{ fileNameW, shellIdListArray }: WindowsClipboardImageFileFormats,
|
||||
{ createImageFromBuffer, openFile }: WindowsClipboardImageFileDeps
|
||||
): Promise<Buffer | null> {
|
||||
if (!hasAtMostOneShellItem(shellIdListArray)) {
|
||||
return null
|
||||
}
|
||||
const filePath = decodeFileNameW(fileNameW)
|
||||
if (!filePath) {
|
||||
return null
|
||||
|
||||
Reference in New Issue
Block a user