Skip to content

Commit a4fa19c

Browse files
committed
pb: Fix bytes copy
1 parent dcb929f commit a4fa19c

File tree

2 files changed

+140
-20
lines changed

2 files changed

+140
-20
lines changed

protobuf/protobuf-core/src/commonTest/kotlin/kotlinx/rpc/protobuf/test/CopyTest.kt

Lines changed: 130 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
package kotlinx.rpc.protobuf.test
66

77
import invoke
8-
import kotlinx.rpc.protobuf.test.invoke
98
import test.submsg.Other
109
import test.submsg.invoke
1110
import kotlin.collections.iterator
@@ -16,19 +15,6 @@ import kotlin.test.assertTrue
1615

1716
class CopyTest {
1817

19-
@Test
20-
fun `test list copy - should be real copy`() {
21-
val userList = mutableListOf(1,2,3)
22-
val original = Repeated {
23-
listInt32 = userList
24-
}
25-
val copy = original.copy()
26-
userList.add(4)
27-
28-
assertEquals(listOf(1,2,3,4), original.listInt32)
29-
assertEquals(listOf(1,2,3), copy.listInt32)
30-
}
31-
3218
@Test
3319
fun `copy primitives and bytes - equality and independence`() {
3420
val bytesSrc = byteArrayOf(1, 2, 3)
@@ -70,6 +56,8 @@ class CopyTest {
7056
}
7157
val cp = orig.copy()
7258

59+
assertEquals(orig, cp)
60+
7361
// Lists are distinct instances
7462
assertTrue(orig.listMessage !== cp.listMessage)
7563
// Elements are deep-copied
@@ -158,4 +146,132 @@ class CopyTest {
158146
assertEquals(1, cp2.RequiredPresence)
159147
assertEquals(5f, cp2.OptionalPresence)
160148
}
149+
150+
@Test
151+
fun `copy with user-provided mutable map - mutation after copy should not affect copy`() {
152+
val userMap = mutableMapOf("a" to 10L, "b" to 20L)
153+
val original = TestMap {
154+
primitives = userMap
155+
}
156+
157+
val copy = original.copy()
158+
159+
// Mutate user's map after copy - should not affect copy
160+
userMap["c"] = 30L
161+
userMap["a"] = 999L
162+
163+
assertEquals(mapOf("a" to 10L, "b" to 20L, "c" to 30L, "a" to 999L), original.primitives)
164+
assertEquals(mapOf("a" to 10L, "b" to 20L), copy.primitives)
165+
}
166+
167+
@Test
168+
fun `copy with user-provided bytes - mutation after copy should not affect copy`() {
169+
val userBytes = byteArrayOf(1, 2, 3)
170+
val original = AllPrimitives {
171+
bytes = userBytes
172+
}
173+
val copy = original.copy()
174+
userBytes[0] = 99
175+
assertEquals(listOf<Byte>(99, 2, 3), original.bytes?.toList())
176+
assertEquals(listOf<Byte>(1, 2, 3), copy.bytes?.toList())
177+
}
178+
179+
@Test
180+
fun `copy with nested messages - user mutation after copy should not affect copy`() {
181+
val userMessages = mutableMapOf(
182+
1 to PresenceCheck { RequiredPresence = 1 },
183+
2 to PresenceCheck { RequiredPresence = 2 }
184+
)
185+
val original = TestMap {
186+
messages = userMessages
187+
}
188+
189+
val copy = original.copy()
190+
191+
// Mutate user's map after copy
192+
userMessages[3] = PresenceCheck { RequiredPresence = 3 }
193+
194+
// Original has all 3, copy should only have original 2
195+
assertEquals(3, original.messages.size)
196+
assertEquals(2, copy.messages.size)
197+
assertEquals(setOf(1, 2, 3), original.messages.keys)
198+
assertEquals(setOf(1, 2), copy.messages.keys)
199+
}
200+
201+
@Test
202+
fun `copy with bytes - mutating source array after construction should not affect copy`() {
203+
val bytesSrc = byteArrayOf(1, 2, 3)
204+
val msg = AllPrimitives {
205+
bytes = bytesSrc
206+
}
207+
208+
val copy = msg.copy()
209+
210+
// Mutate source array after message creation
211+
bytesSrc[0] = 99
212+
bytesSrc[1] = 88
213+
214+
// Neither original nor copy should be affected
215+
assertEquals(listOf<Byte>(99, 88, 3), msg.bytes?.toList())
216+
assertEquals(listOf<Byte>(1, 2, 3), copy.bytes?.toList())
217+
}
218+
219+
@Test
220+
fun `copy with empty collections`() {
221+
val msg = Repeated {
222+
listInt32 = emptyList()
223+
listMessage = emptyList()
224+
}
225+
226+
val copy = msg.copy()
227+
228+
assertTrue(copy.listInt32.isEmpty())
229+
assertTrue(copy.listMessage.isEmpty())
230+
231+
// Verify they are distinct instances
232+
assertTrue(msg.listInt32 !== copy.listInt32)
233+
assertTrue(msg.listMessage !== copy.listMessage)
234+
}
235+
236+
@Test
237+
fun `copy with nested message in list - mutations after copy don't affect copy`() {
238+
val nestedList = mutableListOf(
239+
Repeated.Other { a = 1 },
240+
Repeated.Other { a = 2 }
241+
)
242+
val original = Repeated {
243+
listMessage = nestedList
244+
}
245+
246+
val copy = original.copy()
247+
248+
// Mutate the user's list after copy
249+
nestedList.add(Repeated.Other { a = 3 })
250+
251+
assertEquals(3, original.listMessage.size)
252+
assertEquals(2, copy.listMessage.size)
253+
assertEquals(listOf(1, 2, 3), original.listMessage.map { it.a })
254+
assertEquals(listOf(1, 2), copy.listMessage.map { it.a })
255+
}
256+
257+
@Test
258+
fun `copy lambda modifications don't affect original`() {
259+
val original = AllPrimitives {
260+
int32 = 10
261+
string = "original"
262+
}
263+
264+
val modified = original.copy {
265+
int32 = 20
266+
string = "modified"
267+
}
268+
269+
// Original should remain unchanged
270+
assertEquals(10, original.int32)
271+
assertEquals("original", original.string)
272+
273+
// Modified should have new values
274+
assertEquals(20, modified.int32)
275+
assertEquals("modified", modified.string)
276+
}
161277
}

protoc-gen/protobuf/src/main/kotlin/kotlinx/rpc/protoc/gen/ModelToProtobufKotlinCommonGenerator.kt

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -405,24 +405,28 @@ class ModelToProtobufKotlinCommonGenerator(
405405
// if the field has presence, we need to check if it was set in the original object.
406406
// if it was set, we copy it to the new object, otherwise we leave it unset.
407407
ifBranch(condition = "presenceMask[${field.presenceIdx}]", ifBlock = {
408-
code("copy.${field.name} = ${field.type.copyCall(field.name)}")
408+
code("copy.${field.name} = ${field.type.copyCall(field.name, field.nullable)}")
409409
})
410410
} else {
411411
// by default, we copy the field value
412-
code("copy.${field.name} = ${field.type.copyCall(field.name)}")
412+
code("copy.${field.name} = ${field.type.copyCall(field.name, field.nullable)}")
413413
}
414414
}
415415
code("copy.apply(body)")
416416
code("return copy")
417417
}
418418
}
419419

420-
private fun FieldType.copyCall(varName: String): String {
420+
private fun FieldType.copyCall(varName: String, nullable: Boolean): String {
421421
return when (this) {
422+
FieldType.IntegralType.BYTES -> {
423+
val optionalPrefix = if (nullable) "?" else ""
424+
"$varName$optionalPrefix.copyOf()"
425+
}
422426
is FieldType.IntegralType -> varName
423427
is FieldType.Enum -> varName
424-
is FieldType.List -> "$varName.map { ${value.copyCall("it")} }"
425-
is FieldType.Map -> "$varName.mapValues { ${entry.value.copyCall("it.value")} }"
428+
is FieldType.List -> "$varName.map { ${value.copyCall("it", false)} }"
429+
is FieldType.Map -> "$varName.mapValues { ${entry.value.copyCall("it.value", false)} }"
426430
is FieldType.Message -> "$varName.copy()"
427431
is FieldType.OneOf -> "$varName?.oneOfCopy()"
428432
}
@@ -456,7 +460,7 @@ class ModelToProtobufKotlinCommonGenerator(
456460
// no need to reconstruct a new object, we can just return this
457461
code("this")
458462
} else {
459-
code("$variantName(${variant.type.copyCall("this.value")})")
463+
code("$variantName(${variant.type.copyCall("this.value", false)})")
460464
}
461465
}
462466
}

0 commit comments

Comments
 (0)