fix(windows): paste copied image files (#9640)

* fix(windows): paste copied image files

* fix(windows): reject multi-file image paste

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
OrcaWin 2026-07-20 23:16:36 -04:00 committed by GitHub
parent cabb5dc05a
commit ede57cf723
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 607 additions and 2 deletions

View File

@ -16,8 +16,10 @@ const {
fsReaddirMock,
fsRmMock,
fsWriteFileMock,
fsOpenMock,
fsStatMock,
clipboardReadTextMock,
clipboardReadBufferMock,
clipboardWriteTextMock,
clipboardReadImageMock,
clipboardWriteImageMock,
@ -47,8 +49,10 @@ const {
fsReaddirMock: vi.fn(),
fsRmMock: vi.fn(),
fsWriteFileMock: vi.fn(),
fsOpenMock: vi.fn(),
fsStatMock: vi.fn(),
clipboardReadTextMock: vi.fn(),
clipboardReadBufferMock: vi.fn(),
clipboardWriteTextMock: vi.fn(),
clipboardReadImageMock: vi.fn(),
clipboardWriteImageMock: vi.fn(),
@ -67,6 +71,7 @@ vi.mock('node:fs/promises', () => ({
mkdir: fsMkdirMock,
readdir: fsReaddirMock,
rm: fsRmMock,
open: fsOpenMock,
stat: fsStatMock,
default: {
writeFile: fsWriteFileMock
@ -91,6 +96,7 @@ vi.mock('electron', () => ({
},
clipboard: {
readText: clipboardReadTextMock,
readBuffer: clipboardReadBufferMock,
writeText: clipboardWriteTextMock,
readImage: clipboardReadImageMock,
writeImage: clipboardWriteImageMock,
@ -170,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)
@ -186,9 +198,12 @@ describe('registerClipboardHandlers', () => {
fsRmMock.mockReset()
fsRmMock.mockResolvedValue(undefined)
fsWriteFileMock.mockReset()
fsOpenMock.mockReset()
fsStatMock.mockReset()
fsStatMock.mockResolvedValue({})
clipboardReadTextMock.mockReset()
clipboardReadBufferMock.mockReset()
clipboardReadBufferMock.mockReturnValue(Buffer.alloc(0))
clipboardWriteTextMock.mockReset()
clipboardReadImageMock.mockReset()
clipboardWriteImageMock.mockReset()
@ -542,9 +557,85 @@ describe('registerClipboardHandlers', () => {
handlers.get('clipboard:saveImageAsTempFile')?.(makeClipboardEvent(), undefined)
).resolves.toBe(expectedPath)
expect(fsWriteFileMock).toHaveBeenCalledWith(expectedPath, png)
expect(clipboardReadBufferMock).not.toHaveBeenCalled()
expect(fsOpenMock).not.toHaveBeenCalled()
expect(getSshFilesystemProviderMock).not.toHaveBeenCalled()
})
it('does not inspect FileNameW when an empty image clipboard is read outside Windows', async () => {
const platformSpy = vi.spyOn(process, 'platform', 'get').mockReturnValue('linux')
clipboardReadImageMock.mockReturnValue({ isEmpty: () => true })
try {
registerClipboardHandlers({} as never)
const handler = getRegisteredHandlers().get('clipboard:saveImageAsTempFile')
await expect(handler?.(makeClipboardEvent(), undefined)).resolves.toBeNull()
expect(clipboardReadBufferMock).not.toHaveBeenCalled()
expect(fsOpenMock).not.toHaveBeenCalled()
expect(nativeImageCreateFromBufferMock).not.toHaveBeenCalled()
} finally {
platformSpy.mockRestore()
}
})
it('routes a Windows Explorer FileNameW image through the target-aware attachment flow', async () => {
const platformSpy = vi.spyOn(process, 'platform', 'get').mockReturnValue('win32')
const sourcePath = 'C:\\Users\\alice\\图片\\copied-image.png'
const png = Buffer.from([4, 3, 2, 1])
clipboardReadImageMock.mockReturnValue({ isEmpty: () => true })
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)
source.write('IHDR', 12, 'ascii')
source.writeUInt32BE(1, 16)
source.writeUInt32BE(1, 20)
const close = vi.fn().mockResolvedValue(undefined)
fsOpenMock.mockResolvedValue({
close,
stat: vi.fn().mockResolvedValue({ isFile: () => true, size: source.byteLength }),
read: vi.fn(async (buffer: Buffer, offset: number, length: number, position: number) => {
const bytesRead = Math.min(Math.max(source.byteLength - position, 0), length)
source.copy(buffer, offset, position, position + bytesRead)
return { buffer, bytesRead }
})
})
nativeImageCreateFromBufferMock.mockReturnValue({
getSize: () => ({ height: 1, width: 1 }),
isEmpty: () => false,
toPNG: () => png
})
const writeFileBase64 = vi.fn().mockResolvedValue(undefined)
getSshFilesystemProviderMock.mockReturnValue({
getTempDir: vi.fn().mockResolvedValue('/var/tmp'),
writeFileBase64
})
try {
registerClipboardHandlers({} as never)
const handler = getRegisteredHandlers().get('clipboard:saveImageAsTempFile')
await expect(handler?.(makeClipboardEvent(), { connectionId: 'ssh-1' })).resolves.toBe(
'/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()
expect(writeFileBase64).toHaveBeenCalledWith(
'/var/tmp/orca-paste-1760000000000-00000000-0000-4000-8000-000000000000.png',
png.toString('base64')
)
expect(fsWriteFileMock).not.toHaveBeenCalled()
} finally {
platformSpy.mockRestore()
}
})
it('saves clipboard images through the selected remote runtime host', async () => {
const png = Buffer.alloc(512 * 1024)
const contentBase64 = png.toString('base64')

View File

@ -7,7 +7,7 @@ import {
type WebContents
} from 'electron'
import { spawn } from 'node:child_process'
import { stat } from 'node:fs/promises'
import { open, stat } from 'node:fs/promises'
import type { Store } from '../persistence'
import { isENOENT, PATH_ACCESS_DENIED_MESSAGE, resolveAuthorizedPath } from '../ipc/filesystem-auth'
import {
@ -34,6 +34,7 @@ import {
writeRemoteFileToClipboard
} from './clipboard-remote-file-copy'
import { saveClipboardImageBufferInRuntime } from './clipboard-runtime-image-upload'
import { readWindowsClipboardImageFileAsPng } from './clipboard-windows-image-file'
let trustedClipboardRendererWebContentsId: number | null = null
@ -102,7 +103,20 @@ export function registerClipboardHandlers(store: Store): void {
assertTrustedClipboardSender(event)
const image = clipboard.readImage()
if (image.isEmpty()) {
return null
if (process.platform !== 'win32') {
return null
}
const copiedFilePng = await readWindowsClipboardImageFileAsPng(
{
fileNameW: clipboard.readBuffer('FileNameW'),
shellIdListArray: clipboard.readBuffer('Shell IDList Array')
},
{
createImageFromBuffer: (buffer) => nativeImage.createFromBuffer(buffer),
openFile: (filePath) => open(filePath, 'r')
}
)
return copiedFilePng ? saveClipboardImageBufferForTarget(copiedFilePng, args) : null
}
assertClipboardImageDimensionsWithinLimit(image.getSize())
return saveClipboardImageBufferForTarget(image.toPNG(), args)

View File

@ -0,0 +1,282 @@
import { describe, expect, it, vi } from 'vitest'
import {
CLIPBOARD_IMAGE_MAX_PIXELS,
CLIPBOARD_IMAGE_MAX_SOURCE_BYTES
} from '../../shared/clipboard-image'
import { readWindowsClipboardImageFileAsPng } from './clipboard-windows-image-file'
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)
source.writeUInt32BE(13, 8)
source.write('IHDR', 12, 'ascii')
source.writeUInt32BE(width, 16)
source.writeUInt32BE(height, 20)
return source
}
function jpegHeader(width = 10, height = 10): Buffer {
return Buffer.from([
0xff,
0xd8,
0xff,
0xe0,
0x00,
0x02,
0xff,
0xc2,
0x00,
0x07,
0x08,
height >> 8,
height & 0xff,
width >> 8,
width & 0xff
])
}
function image(png = Buffer.from([4, 3, 2, 1])) {
return {
getSize: () => ({ height: 10, width: 10 }),
isEmpty: () => false,
toPNG: () => png
}
}
function fileHandle(
source: Buffer,
options: { chunkSize?: number; isFile?: boolean; size?: number } = {}
) {
const close = vi.fn().mockResolvedValue(undefined)
const read = vi.fn(async (buffer: Buffer, offset: number, length: number, position: number) => {
const bytesRead = Math.min(
Math.max(source.byteLength - position, 0),
length,
options.chunkSize ?? Number.POSITIVE_INFINITY
)
source.copy(buffer, offset, position, position + bytesRead)
return { buffer, bytesRead }
})
return {
close,
read,
stat: vi.fn().mockResolvedValue({
isFile: () => options.isFile ?? true,
size: options.size ?? source.byteLength
})
}
}
describe('readWindowsClipboardImageFileAsPng', () => {
it.each([
'C:\\Users\\alice\\图片\\shot.PNG',
'\\\\server\\share\\shot.jpeg',
'\\\\?\\C:\\Users\\alice\\shot.jpg',
'\\\\?\\UNC\\server\\share\\shot.png'
])('decodes and converts a bounded FileNameW path: %s', async (filePath) => {
const source = filePath.toLowerCase().endsWith('.png') ? pngHeader() : jpegHeader()
const png = Buffer.from([9, 8, 7])
const handle = fileHandle(source, { chunkSize: 3 })
const openFile = vi.fn().mockResolvedValue(handle)
const createImageFromBuffer = vi.fn(() => image(png) as never)
await expect(
readWindowsClipboardImageFileAsPng(clipboardFormats(filePath), {
createImageFromBuffer,
openFile
})
).resolves.toEqual(png)
expect(openFile).toHaveBeenCalledWith(filePath)
expect(handle.read.mock.calls.length).toBeGreaterThan(1)
expect(handle.close).toHaveBeenCalledOnce()
expect(createImageFromBuffer).toHaveBeenCalledWith(source)
})
it.each([
['empty', Buffer.alloc(0)],
['odd byte count', Buffer.from([65, 0, 0])],
['missing terminator', Buffer.from('C:\\shot.png', 'utf16le')],
['relative path', fileNameW('shot.png')],
['drive-relative path', fileNameW('C:shot.png')],
['rooted drive-relative path', fileNameW('\\shot.png')],
['device namespace', fileNameW('\\\\.\\pipe\\shot.png')],
['UNC named-pipe namespace', fileNameW('\\\\server\\pipe\\shot.png')],
['extended UNC named-pipe namespace', fileNameW('\\\\?\\UNC\\server\\pipe\\shot.png')],
['multiple paths', Buffer.from('C:\\one.png\0C:\\two.png\0', 'utf16le')],
['unsupported image type', fileNameW('C:\\shot.webp')],
['oversized payload', Buffer.alloc(64 * 1024 + 2)]
])('ignores malformed or unsupported %s payloads', async (_name, payload) => {
const openFile = vi.fn()
const createImageFromBuffer = vi.fn()
await expect(
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()
expect(createImageFromBuffer).not.toHaveBeenCalled()
})
it('ignores missing, inaccessible, and looping paths', async () => {
for (const code of ['ENOENT', 'EACCES', 'ELOOP']) {
const error = Object.assign(new Error(code), { code })
const openFile = vi.fn().mockRejectedValue(error)
await expect(
readWindowsClipboardImageFileAsPng(clipboardFormats('C:\\shot.png'), {
createImageFromBuffer: vi.fn(),
openFile
})
).resolves.toBeNull()
}
})
it('ignores directories and closes their handles', async () => {
const handle = fileHandle(Buffer.alloc(0), { isFile: false })
const createImageFromBuffer = vi.fn()
await expect(
readWindowsClipboardImageFileAsPng(clipboardFormats('C:\\images.png'), {
createImageFromBuffer,
openFile: vi.fn().mockResolvedValue(handle)
})
).resolves.toBeNull()
expect(handle.read).not.toHaveBeenCalled()
expect(handle.close).toHaveBeenCalledOnce()
expect(createImageFromBuffer).not.toHaveBeenCalled()
})
it('rejects oversized sources before reading or decoding', async () => {
const handle = fileHandle(Buffer.alloc(0), {
size: CLIPBOARD_IMAGE_MAX_SOURCE_BYTES + 1
})
const createImageFromBuffer = vi.fn()
await expect(
readWindowsClipboardImageFileAsPng(clipboardFormats('C:\\huge.png'), {
createImageFromBuffer,
openFile: vi.fn().mockResolvedValue(handle)
})
).rejects.toThrow('Clipboard image is too large')
expect(handle.read).not.toHaveBeenCalled()
expect(handle.close).toHaveBeenCalledOnce()
expect(createImageFromBuffer).not.toHaveBeenCalled()
})
it('ignores a source that changes size after handle validation', async () => {
const handle = fileHandle(Buffer.from('grew'), { size: 3 })
const createImageFromBuffer = vi.fn()
await expect(
readWindowsClipboardImageFileAsPng(clipboardFormats('C:\\changed.png'), {
createImageFromBuffer,
openFile: vi.fn().mockResolvedValue(handle)
})
).resolves.toBeNull()
expect(handle.close).toHaveBeenCalledOnce()
expect(createImageFromBuffer).not.toHaveBeenCalled()
})
it('ignores image bytes the native decoder cannot decode', async () => {
const handle = fileHandle(Buffer.from('not-an-image'))
await expect(
readWindowsClipboardImageFileAsPng(clipboardFormats('C:\\invalid.png'), {
createImageFromBuffer: vi.fn(() => ({ isEmpty: () => true }) as never),
openFile: vi.fn().mockResolvedValue(handle)
})
).resolves.toBeNull()
})
it('rejects oversized encoded dimensions before native decoding', async () => {
const handle = fileHandle(pngHeader(CLIPBOARD_IMAGE_MAX_PIXELS + 1, 1))
const createImageFromBuffer = vi.fn()
await expect(
readWindowsClipboardImageFileAsPng(clipboardFormats('C:\\pixel-bomb.png'), {
createImageFromBuffer,
openFile: vi.fn().mockResolvedValue(handle)
})
).rejects.toThrow('Clipboard image is too large')
expect(createImageFromBuffer).not.toHaveBeenCalled()
expect(handle.close).toHaveBeenCalledOnce()
})
it('bounds malformed JPEG marker scanning before native decoding', async () => {
const source = Buffer.alloc(2 + 2 * 4097)
source.set([0xff, 0xd8])
for (let offset = 2; offset < source.byteLength; offset += 2) {
source.set([0xff, 0x01], offset)
}
const createImageFromBuffer = vi.fn()
await expect(
readWindowsClipboardImageFileAsPng(clipboardFormats('C:\\marker-flood.jpg'), {
createImageFromBuffer,
openFile: vi.fn().mockResolvedValue(fileHandle(source))
})
).resolves.toBeNull()
expect(createImageFromBuffer).not.toHaveBeenCalled()
})
it('bounds decoded dimensions and converted PNG bytes', async () => {
const oversizedDimensions = fileHandle(pngHeader())
await expect(
readWindowsClipboardImageFileAsPng(clipboardFormats('C:\\wide.png'), {
createImageFromBuffer: vi.fn(
() =>
({
getSize: () => ({ height: 1, width: CLIPBOARD_IMAGE_MAX_PIXELS + 1 }),
isEmpty: () => false,
toPNG: vi.fn()
}) as never
),
openFile: vi.fn().mockResolvedValue(oversizedDimensions)
})
).rejects.toThrow('Clipboard image is too large')
const oversizedPng = fileHandle(pngHeader())
await expect(
readWindowsClipboardImageFileAsPng(clipboardFormats('C:\\expanded.png'), {
createImageFromBuffer: vi.fn(
() => image(Buffer.alloc(CLIPBOARD_IMAGE_MAX_SOURCE_BYTES + 1)) as never
),
openFile: vi.fn().mockResolvedValue(oversizedPng)
})
).rejects.toThrow('Clipboard image is too large')
})
})

View File

@ -0,0 +1,218 @@
import { win32 } from 'node:path'
import type { FileHandle } from 'node:fs/promises'
import type { NativeImage } from 'electron'
import {
assertClipboardImageByteLengthWithinLimit,
assertClipboardImageDimensionsWithinLimit
} from '../../shared/clipboard-image'
type ClipboardImageFileHandle = Pick<FileHandle, 'close' | 'read' | 'stat'>
type WindowsClipboardImageFileDeps = {
createImageFromBuffer: (buffer: Buffer) => NativeImage
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'])
const JPEG_DIMENSION_SCAN_MAX_BYTES = 1024 * 1024
const JPEG_DIMENSION_SCAN_MAX_MARKERS = 4096
const PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])
const JPEG_START_OF_FRAME_MARKERS = new Set([
0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7, 0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf
])
function isOrdinaryUncShare(share: string | undefined): boolean {
return typeof share === 'string' && share.toLowerCase() !== 'pipe'
}
function isFullyQualifiedWindowsPath(filePath: string): boolean {
if (/^[A-Za-z]:[\\/]/.test(filePath)) {
return true
}
if (/^\\\\\?\\[A-Za-z]:\\/.test(filePath)) {
return true
}
const extendedUnc = /^\\\\\?\\UNC\\[^\\/]+\\([^\\/]+)(?:\\|$)/i.exec(filePath)
if (extendedUnc) {
return isOrdinaryUncShare(extendedUnc[1])
}
const unc = /^[/\\]{2}(?![?.][/\\])[^/\\]+[/\\]([^/\\]+)(?:[/\\]|$)/.exec(filePath)
return isOrdinaryUncShare(unc?.[1])
}
function decodeFileNameW(value: Buffer): string | null {
if (
value.byteLength < 2 ||
value.byteLength > FILE_NAME_W_MAX_BYTES ||
value.byteLength % 2 !== 0 ||
value.readUInt16LE(value.byteLength - 2) !== 0
) {
return null
}
let end = value.byteLength - 2
while (end >= 2 && value.readUInt16LE(end - 2) === 0) {
end -= 2
}
const filePath = value.subarray(0, end).toString('utf16le')
if (!filePath || filePath.includes('\0') || !isFullyQualifiedWindowsPath(filePath)) {
return 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 ||
!source.subarray(0, PNG_SIGNATURE.byteLength).equals(PNG_SIGNATURE) ||
source.readUInt32BE(8) !== 13 ||
source.toString('ascii', 12, 16) !== 'IHDR'
) {
return null
}
return { height: source.readUInt32BE(20), width: source.readUInt32BE(16) }
}
function readJpegDimensions(source: Buffer): { height: number; width: number } | null {
if (source.byteLength < 4 || source[0] !== 0xff || source[1] !== 0xd8) {
return null
}
let offset = 2
let markersRead = 0
const scanEnd = Math.min(source.byteLength, JPEG_DIMENSION_SCAN_MAX_BYTES)
while (offset < scanEnd && markersRead < JPEG_DIMENSION_SCAN_MAX_MARKERS) {
while (offset < scanEnd && source[offset] === 0xff) {
offset += 1
}
const marker = source[offset]
offset += 1
markersRead += 1
if (marker === undefined || marker === 0x00 || marker === 0xd9 || marker === 0xda) {
return null
}
if (marker === 0x01 || (marker >= 0xd0 && marker <= 0xd8)) {
continue
}
if (offset + 2 > source.byteLength) {
return null
}
const segmentLength = source.readUInt16BE(offset)
if (
segmentLength < 2 ||
offset + segmentLength > source.byteLength ||
offset + segmentLength > scanEnd
) {
return null
}
if (JPEG_START_OF_FRAME_MARKERS.has(marker)) {
if (segmentLength < 7) {
return null
}
return { height: source.readUInt16BE(offset + 3), width: source.readUInt16BE(offset + 5) }
}
offset += segmentLength
}
return null
}
function readImageDimensions(source: Buffer): { height: number; width: number } | null {
return readPngDimensions(source) ?? readJpegDimensions(source)
}
async function readStableFile(
handle: ClipboardImageFileHandle,
expectedSize: number
): Promise<Buffer | null> {
const buffer = Buffer.alloc(expectedSize + 1)
let bytesRead = 0
let readCalls = 0
while (bytesRead < buffer.byteLength && readCalls < FILE_READ_MAX_CALLS) {
const result = await handle.read(buffer, bytesRead, buffer.byteLength - bytesRead, bytesRead)
readCalls += 1
if (result.bytesRead === 0) {
break
}
bytesRead += result.bytesRead
}
return bytesRead === expectedSize ? buffer.subarray(0, bytesRead) : null
}
export async function readWindowsClipboardImageFileAsPng(
{ fileNameW, shellIdListArray }: WindowsClipboardImageFileFormats,
{ createImageFromBuffer, openFile }: WindowsClipboardImageFileDeps
): Promise<Buffer | null> {
if (!hasAtMostOneShellItem(shellIdListArray)) {
return null
}
const filePath = decodeFileNameW(fileNameW)
if (!filePath) {
return null
}
let handle: ClipboardImageFileHandle
try {
// Why: one handle keeps validation and the bounded read on the same file if its path changes.
handle = await openFile(filePath)
} catch {
return null
}
let source: Buffer | null = null
try {
let file: Awaited<ReturnType<ClipboardImageFileHandle['stat']>>
try {
file = await handle.stat()
} catch {
return null
}
if (!file.isFile() || !Number.isSafeInteger(file.size) || file.size < 0) {
return null
}
assertClipboardImageByteLengthWithinLimit(file.size)
try {
source = await readStableFile(handle, file.size)
} catch {
return null
}
} finally {
await handle.close().catch(() => {})
}
if (!source) {
return null
}
const encodedDimensions = readImageDimensions(source)
if (!encodedDimensions) {
return null
}
// Why: reject pixel bombs from metadata before NativeImage allocates decoded pixels.
assertClipboardImageDimensionsWithinLimit(encodedDimensions)
let image: NativeImage
try {
image = createImageFromBuffer(source)
} catch {
return null
}
if (image.isEmpty()) {
return null
}
assertClipboardImageDimensionsWithinLimit(image.getSize())
const png = image.toPNG()
assertClipboardImageByteLengthWithinLimit(png.byteLength)
return png
}