1+ import { Buffer } from 'node:buffer'
2+
13export type EditContentFailure =
24 | { reason : 'empty_search' }
35 | { reason : 'not_found' }
46 | { reason : 'ambiguous' ; lineNumbers : number [ ] }
57 | { reason : 'invalid_occurrence' }
68 | { reason : 'invalid_anchor_order' }
9+ | { reason : 'output_too_large' ; maxBytes : number }
10+
11+ export interface EditContentOptions {
12+ maxOutputBytes ?: number
13+ }
714
815export class EditContentError extends Error {
916 constructor (
@@ -61,6 +68,30 @@ function scanMatches(text: string, search: string): MatchScan {
6168 return { count, lineNumbers }
6269}
6370
71+ function assertReplacementOutputWithinLimit (
72+ text : string ,
73+ search : string ,
74+ content : string ,
75+ count : number ,
76+ maxOutputBytes : number | undefined
77+ ) : void {
78+ if ( maxOutputBytes === undefined ) return
79+
80+ const retainedBytes = Buffer . byteLength ( text ) - count * Buffer . byteLength ( search )
81+ const replacementBytes = Buffer . byteLength ( content )
82+ const availableReplacementBytes = maxOutputBytes - retainedBytes
83+ const exceedsLimit =
84+ availableReplacementBytes < 0 ||
85+ ( replacementBytes > 0 && count > Math . floor ( availableReplacementBytes / replacementBytes ) )
86+
87+ if ( exceedsLimit ) {
88+ throw new EditContentError ( `Edit result exceeds the ${ maxOutputBytes } byte limit` , {
89+ reason : 'output_too_large' ,
90+ maxBytes : maxOutputBytes ,
91+ } )
92+ }
93+ }
94+
6495/**
6596 * Replaces the single occurrence of `search`, or refuses.
6697 *
@@ -72,7 +103,8 @@ export function applyStringReplacement(
72103 text : string ,
73104 search : string ,
74105 content : string ,
75- replaceAll = false
106+ replaceAll = false ,
107+ options ?: EditContentOptions
76108) : string {
77109 if ( search . length === 0 ) {
78110 throw new EditContentError ( 'Search text cannot be empty' , { reason : 'empty_search' } )
@@ -94,7 +126,15 @@ export function applyStringReplacement(
94126 )
95127 }
96128
97- if ( replaceAll ) return text . split ( search ) . join ( content )
129+ assertReplacementOutputWithinLimit (
130+ text ,
131+ search ,
132+ content ,
133+ replaceAll ? count : 1 ,
134+ options ?. maxOutputBytes
135+ )
136+
137+ if ( replaceAll ) return text . replaceAll ( search , content )
98138
99139 const index = text . indexOf ( search )
100140 return text . slice ( 0 , index ) + content + text . slice ( index + search . length )
@@ -188,10 +228,11 @@ function contentLines(content: string): string[] {
188228 */
189229export function applyWorkspaceFileContentEdit (
190230 text : string ,
191- edit : WorkspaceFileContentEdit
231+ edit : WorkspaceFileContentEdit ,
232+ options ?: EditContentOptions
192233) : string {
193234 if ( edit . mode === 'search_replace' ) {
194- return applyStringReplacement ( text , edit . search , edit . content , edit . replaceAll )
235+ return applyStringReplacement ( text , edit . search , edit . content , edit . replaceAll , options )
195236 }
196237
197238 const lines = splitLines ( text )
0 commit comments