@@ -19,29 +19,27 @@ local DA_PATCH = 2
1919-- Private ----------------------------------------------------------------
2020
2121local alloc = Base .alloc
22+ local ensure = Base .ensure
2223local allocDeltaId = Shared .allocDeltaId
2324local acquireScratch = Shared .acquireScratch
2425local releaseScratch = Shared .releaseScratch
2526local rangeEqual = Shared .rangeEqual
2627local copyToBuf = Shared .copyToBuf
28+ local containsDelta = Shared .containsDelta
29+ local enforceMaxCount = Shared .enforceMaxCount
2730local boolBytes = Shared .boolBytes
2831local packBoolArray = Shared .packBoolArray
2932local unpackBoolArray = Shared .unpackBoolArray
3033local varintRead = Varint .read
3134local varintWrite = Varint .write
35+ local varintLength = Varint .length
3236local writeu8 = buffer.writeu8
3337local readu8 = buffer.readu8
3438local bufCopy = buffer.copy
3539local bufLen = buffer.len
3640
3741type DeltaArrayEntry = { eBuf : buffer , eLen : number }
3842
39- local function ensure (ch : Types .ChannelState , n : number ): ()
40- if ch .cursor + n > ch .size then
41- alloc (ch , n )
42- end
43- end
44-
4543-- Bit-packed bool element fast path: 1 bit per entry instead of 1 byte.
4644local function makeBoolArray (
4745 element : Types .InternalCodec <any >,
@@ -60,9 +58,7 @@ local function makeBoolArray(
6058
6159 read = function (src : buffer , pos : number , _refs : { Instance }? ): ({ boolean }, number )
6260 local len , lenBytes = varintRead (src , pos )
63- if maxCount and len > maxCount then
64- Log .error (`count {len } exceeds max {maxCount }` )
65- end
61+ enforceMaxCount (len , maxCount )
6662 if len == 0 then
6763 return {}, lenBytes
6864 end
@@ -115,9 +111,7 @@ local function makeDirectArray(
115111
116112 read = function (src : buffer , pos : number , _refs : { Instance }? ): ({ any }, number )
117113 local len , lenBytes = varintRead (src , pos )
118- if maxCount and len > maxCount then
119- Log .error (`count {len } exceeds max {maxCount }` )
120- end
114+ enforceMaxCount (len , maxCount )
121115 if len == 0 then
122116 return {}, lenBytes
123117 end
@@ -159,9 +153,7 @@ local function makeGenericArray(
159153
160154 read = function (src : buffer , pos : number , refs : { Instance }? ): ({ any }, number )
161155 local len , lenBytes = varintRead (src , pos )
162- if maxCount and len > maxCount then
163- Log .error (`count {len } exceeds max {maxCount }` )
164- end
156+ enforceMaxCount (len , maxCount )
165157 if len == 0 then
166158 return {}, lenBytes
167159 end
@@ -202,7 +194,7 @@ function Array.array(element: Types.InternalCodec<any>, maxCount: number?): Type
202194 if element ._hasUnknown then
203195 codec ._hasUnknown = true
204196 end
205- if element . _isDelta or element . _hasDelta then
197+ if containsDelta ( element ) then
206198 codec ._hasDelta = true
207199 end
208200 return table.freeze (codec )
@@ -225,6 +217,11 @@ function Array.deltaArray(
225217 element : Types .InternalCodec <any >,
226218 maxCount : number ?
227219): Types .InternalCodec <any >
220+ -- Inner delta would chain-diff across elements within a frame, not across frames.
221+ if containsDelta (element ) then
222+ Log .error ("deltaArray element cannot contain delta state" )
223+ end
224+
228225 local deltaId = allocDeltaId ()
229226 local inner = Array .array (element , maxCount )
230227
@@ -238,9 +235,7 @@ function Array.deltaArray(
238235 value : { any }
239236 ): (number , { number }, { number })
240237 local len = # value
241- if maxCount and len > maxCount then
242- Log .error (`count {len } exceeds max {maxCount }` )
243- end
238+ enforceMaxCount (len , maxCount )
244239 local off = table.create (len )
245240 local elen = table.create (len )
246241 for i = 1 , len do
@@ -275,11 +270,9 @@ function Array.deltaArray(
275270 ch .cursor += 1
276271 varintWrite (ch , len )
277272 local payloadLen = scratch .cursor
278- if payloadLen > 0 then
279- ensure (ch , payloadLen )
280- bufCopy (ch .buff , ch .cursor , scratchBuf , 0 , payloadLen )
281- ch .cursor += payloadLen
282- end
273+ ensure (ch , payloadLen )
274+ bufCopy (ch .buff , ch .cursor , scratchBuf , 0 , payloadLen )
275+ ch .cursor += payloadLen
283276
284277 local perIdx : { [number ]: DeltaArrayEntry } = {}
285278 for i = 1 , len do
@@ -323,20 +316,17 @@ function Array.deltaArray(
323316 return
324317 end
325318
326- -- PATCH cost vs FULL: every changed index pays an index varint.
327- -- When index headers approach the count cost, FULL wins outright.
328- if changedCount * 2 >= len then
329- ensure (ch , 1 )
330- writeu8 (ch .buff , ch .cursor , DA_FULL )
331- ch .cursor += 1
332- varintWrite (ch , len )
333- local payloadLen = scratch .cursor
334- if payloadLen > 0 then
335- ensure (ch , payloadLen )
336- bufCopy (ch .buff , ch .cursor , scratchBuf , 0 , payloadLen )
337- ch .cursor += payloadLen
338- end
339- else
319+ -- Pick FULL vs PATCH on actual byte cost. Both share `1 + varintLength(len)`
320+ -- so it cancels: compare PATCH's index-header overhead + changed payload
321+ -- against FULL's full payload.
322+ local fullPayload = scratch .cursor
323+ local patchPayload = varintLength (changedCount )
324+ for i = 1 , changedCount do
325+ local idx = changed [i ]
326+ patchPayload += varintLength (idx ) + elen [idx ]
327+ end
328+
329+ if patchPayload < fullPayload then
340330 ensure (ch , 1 )
341331 writeu8 (ch .buff , ch .cursor , DA_PATCH )
342332 ch .cursor += 1
@@ -346,12 +336,18 @@ function Array.deltaArray(
346336 local idx = changed [i ]
347337 varintWrite (ch , idx )
348338 local n = elen [idx ]
349- if n > 0 then
350- ensure (ch , n )
351- bufCopy (ch .buff , ch .cursor , scratchBuf , off [idx ], n )
352- ch .cursor += n
353- end
339+ ensure (ch , n )
340+ bufCopy (ch .buff , ch .cursor , scratchBuf , off [idx ], n )
341+ ch .cursor += n
354342 end
343+ else
344+ ensure (ch , 1 )
345+ writeu8 (ch .buff , ch .cursor , DA_FULL )
346+ ch .cursor += 1
347+ varintWrite (ch , len )
348+ ensure (ch , fullPayload )
349+ bufCopy (ch .buff , ch .cursor , scratchBuf , 0 , fullPayload )
350+ ch .cursor += fullPayload
355351 end
356352
357353 -- Update cache: refresh changed (and appended), drop truncated.
@@ -412,19 +408,29 @@ function Array.deltaArray(
412408 if lLen == 0 then
413409 Log .error ("truncated deltaArray length" )
414410 end
411+ enforceMaxCount (newLen , maxCount , "newLen" )
415412 total += lLen
416413
417414 local changeCount , cLen = varintRead (src , pos + total )
418415 if cLen == 0 then
419416 Log .error ("truncated deltaArray change count" )
420417 end
418+ -- changeCount is also bounded by newLen: every index appears at most once.
419+ if changeCount > newLen then
420+ Log .error (`deltaArray changeCount {changeCount } exceeds newLen {newLen }` )
421+ end
421422 total += cLen
422423
423424 for _ = 1 , changeCount do
424425 local idx , iLen = varintRead (src , pos + total )
425426 if iLen == 0 then
426427 Log .error ("truncated deltaArray index" )
427428 end
429+ -- Reject out-of-range idx so the wire can't poison the cache with
430+ -- arbitrary keys that survive truncation.
431+ if idx < 1 or idx > newLen then
432+ Log .error (`deltaArray index {idx } out of range [1, {newLen }]` )
433+ end
428434 total += iLen
429435 local v , vc = element .read (src , pos + total , refs )
430436 if vc == 0 then
0 commit comments