Files
orca/src/shared/large-diff-render-limit.ts
T
Jinjing aee2c0a6a4 Limit large git diff payloads in main process before IPC transfer (#5469)
* Limit large git diff payloads in main process before IPC transfer

Move the large diff rendering limit check to a shared module so that
the main process can evaluate diff sizes before transferring them. If
a diff exceeds the limits, its text content is dropped prior to IPC
serialization, and only the limit metadata is sent. This prevents the
application from freezing or crashing when loading massive diff files.

* Gracefully handle and prune oversized files in diff viewer

Prevent UI freezes and out-of-memory errors when viewing or editing
extremely large diffs or files. Working-tree files above 10MB and git
buffer overflows are treated as binary. Text diffs exceeding safe
rendering limits have their contents pruned before IPC transport, and
the UI is updated to show fallback states and disable invalid saves.

* Document save action check for large diffs and fix test import

Explain why saveContentAvailable is required for oversized diffs, as
stripped text bodies before IPC prevent complete saves. Also update the
large-diff-render-limit import in E2E tests to use the shared path.
2026-06-15 22:13:46 -07:00

197 lines
4.4 KiB
TypeScript

export const MAX_RENDERED_DIFF_LINES_PER_SIDE = 120_000
export const MAX_RENDERED_DIFF_COMBINED_CHARACTERS = 6_000_000
export type LargeDiffRenderLimitReason = 'line-count' | 'character-count'
export type DiffLineCounts = {
original: number
modified: number
}
export type DiffLineCountMinimums = {
original: boolean
modified: boolean
}
export type LargeDiffRenderLimit =
| {
limited: false
lineCounts: DiffLineCounts
characterCount: number
}
| {
limited: true
reason: LargeDiffRenderLimitReason
lineCounts: DiffLineCounts | null
lineCountsAreMinimum?: DiffLineCountMinimums
characterCount: number
limits: {
maxLinesPerSide: number
maxCombinedCharacters: number
}
}
export function countLinesEmptyAsZero(content: string): number {
if (content.length === 0) {
return 0
}
let lineCount = 1
for (let index = 0; index < content.length; index += 1) {
if (content.charCodeAt(index) === 10) {
lineCount += 1
}
}
return lineCount
}
type BoundedLineCount = {
count: number
exceeded: boolean
}
export function countLinesEmptyAsZeroUpToLimit(
content: string,
maxLines: number
): BoundedLineCount {
if (content.length === 0) {
return { count: 0, exceeded: false }
}
let lineCount = 1
for (let index = 0; index < content.length; index += 1) {
if (content.charCodeAt(index) !== 10) {
continue
}
lineCount += 1
if (lineCount > maxLines) {
return { count: lineCount, exceeded: true }
}
}
return { count: lineCount, exceeded: false }
}
export function countLinesLikeSplit(content: string): number {
let lineCount = 1
for (let index = 0; index < content.length; index += 1) {
if (content.charCodeAt(index) === 10) {
lineCount += 1
}
}
return lineCount
}
type LargeDiffRenderLimitInput = {
originalContent: string
modifiedContent: string
}
type LargeDiffRenderLimitCountsInput = {
originalLineCount: number
modifiedLineCount: number
originalCharacterCount: number
modifiedCharacterCount: number
}
export function getLargeDiffRenderLimitFromCounts({
originalLineCount,
modifiedLineCount,
originalCharacterCount,
modifiedCharacterCount
}: LargeDiffRenderLimitCountsInput): LargeDiffRenderLimit {
const lineCounts = {
original: originalLineCount,
modified: modifiedLineCount
}
const characterCount = originalCharacterCount + modifiedCharacterCount
const limits = {
maxLinesPerSide: MAX_RENDERED_DIFF_LINES_PER_SIDE,
maxCombinedCharacters: MAX_RENDERED_DIFF_COMBINED_CHARACTERS
}
if (
lineCounts.original > MAX_RENDERED_DIFF_LINES_PER_SIDE ||
lineCounts.modified > MAX_RENDERED_DIFF_LINES_PER_SIDE
) {
return {
limited: true,
reason: 'line-count',
lineCounts,
characterCount,
limits
}
}
if (characterCount > MAX_RENDERED_DIFF_COMBINED_CHARACTERS) {
return {
limited: true,
reason: 'character-count',
lineCounts,
characterCount,
limits
}
}
return {
limited: false,
lineCounts,
characterCount
}
}
export function getLargeDiffRenderLimit({
originalContent,
modifiedContent
}: LargeDiffRenderLimitInput): LargeDiffRenderLimit {
const characterCount = originalContent.length + modifiedContent.length
const limits = {
maxLinesPerSide: MAX_RENDERED_DIFF_LINES_PER_SIDE,
maxCombinedCharacters: MAX_RENDERED_DIFF_COMBINED_CHARACTERS
}
if (characterCount > MAX_RENDERED_DIFF_COMBINED_CHARACTERS) {
return {
limited: true,
reason: 'character-count',
lineCounts: null,
characterCount,
limits
}
}
const originalLineCount = countLinesEmptyAsZeroUpToLimit(
originalContent,
MAX_RENDERED_DIFF_LINES_PER_SIDE
)
const modifiedLineCount = countLinesEmptyAsZeroUpToLimit(
modifiedContent,
MAX_RENDERED_DIFF_LINES_PER_SIDE
)
if (originalLineCount.exceeded || modifiedLineCount.exceeded) {
return {
limited: true,
reason: 'line-count',
lineCounts: {
original: originalLineCount.count,
modified: modifiedLineCount.count
},
lineCountsAreMinimum: {
original: originalLineCount.exceeded,
modified: modifiedLineCount.exceeded
},
characterCount,
limits
}
}
return {
limited: false,
lineCounts: {
original: originalLineCount.count,
modified: modifiedLineCount.count
},
characterCount
}
}