Skip to content

Commit ffc69db

Browse files
committed
Fix offset bugs in MessagePackGenerator and document pre-existing issues
Three bugs where non-zero offset/position was ignored: - getBytesIfAscii: bytes[i] -> bytes[i - offset] - writeByteArrayTextValue: store slice instead of whole array - ByteBuffer: use arrayOffset() + position() as payload start All three bugs exist identically in msgpack-jackson; documented in plans/preexisting-issues.md with practical impact notes.
1 parent ab62c4d commit ffc69db

3 files changed

Lines changed: 248 additions & 3 deletions

File tree

msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,7 @@ else if (v instanceof ByteBuffer) {
390390
int len = bb.remaining();
391391
if (bb.hasArray()) {
392392
messagePacker.packBinaryHeader(len);
393-
messagePacker.writePayload(bb.array(), bb.arrayOffset(), len);
393+
messagePacker.writePayload(bb.array(), bb.arrayOffset() + bb.position(), len);
394394
}
395395
else {
396396
byte[] data = new byte[len];
@@ -496,7 +496,7 @@ private byte[] getBytesIfAscii(char[] chars, int offset, int len)
496496
if (c >= 0x80) {
497497
return null;
498498
}
499-
bytes[i] = (byte) c;
499+
bytes[i - offset] = (byte) c;
500500
}
501501
return bytes;
502502
}
@@ -524,7 +524,9 @@ private void writeCharArrayTextValue(char[] text, int offset, int len) throws IO
524524
private void writeByteArrayTextValue(byte[] text, int offset, int len) throws IOException
525525
{
526526
if (areAllAsciiBytes(text, offset, len)) {
527-
addValueNode(new AsciiCharString(text));
527+
byte[] slice = new byte[len];
528+
System.arraycopy(text, offset, slice, 0, len);
529+
addValueNode(new AsciiCharString(slice));
528530
return;
529531
}
530532
addValueNode(new String(text, offset, len, DEFAULT_CHARSET));

msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackGeneratorTest.java

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -956,4 +956,97 @@ public String getName()
956956
return name;
957957
}
958958
}
959+
960+
@Test
961+
public void testWriteStringCharArrayWithOffset()
962+
throws IOException
963+
{
964+
// Padding chars before/after the actual content to test non-zero offset
965+
char[] buf = new char[] {'X', 'X', 'h', 'e', 'l', 'l', 'o', 'X'};
966+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
967+
JsonGenerator generator = factory.createGenerator(baos, JsonEncoding.UTF8);
968+
generator.writeStartArray();
969+
generator.writeString(buf, 2, 5); // "hello"
970+
generator.writeEndArray();
971+
generator.close();
972+
973+
MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(baos.toByteArray());
974+
unpacker.unpackArrayHeader();
975+
assertEquals("hello", unpacker.unpackString());
976+
}
977+
978+
@Test
979+
public void testWriteStringCharArrayWithOffsetNonAscii()
980+
throws IOException
981+
{
982+
// Non-ASCII to exercise the non-fast-path in getBytesIfAscii
983+
char[] buf = new char[] {'X', '三', '四', '五', 'X'}; // 三四五
984+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
985+
JsonGenerator generator = factory.createGenerator(baos, JsonEncoding.UTF8);
986+
generator.writeStartArray();
987+
generator.writeString(buf, 1, 3);
988+
generator.writeEndArray();
989+
generator.close();
990+
991+
MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(baos.toByteArray());
992+
unpacker.unpackArrayHeader();
993+
assertEquals("三四五", unpacker.unpackString());
994+
}
995+
996+
@Test
997+
public void testWriteUTF8StringWithOffset()
998+
throws IOException
999+
{
1000+
// Padding bytes before/after to test non-zero offset in writeUTF8String
1001+
byte[] buf = new byte[] {'X', 'X', 'h', 'i', 'X'};
1002+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
1003+
JsonGenerator generator = factory.createGenerator(baos, JsonEncoding.UTF8);
1004+
generator.writeStartArray();
1005+
generator.writeUTF8String(buf, 2, 2); // "hi"
1006+
generator.writeEndArray();
1007+
generator.close();
1008+
1009+
MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(baos.toByteArray());
1010+
unpacker.unpackArrayHeader();
1011+
assertEquals("hi", unpacker.unpackString());
1012+
}
1013+
1014+
@Test
1015+
public void testWriteBinaryWithOffset()
1016+
throws IOException
1017+
{
1018+
byte[] data = new byte[] {0x00, 0x01, 0x02, 0x03, 0x04};
1019+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
1020+
JsonGenerator generator = factory.createGenerator(baos, JsonEncoding.UTF8);
1021+
generator.writeStartArray();
1022+
generator.writeBinary(data, 1, 3); // bytes 0x01, 0x02, 0x03
1023+
generator.writeEndArray();
1024+
generator.close();
1025+
1026+
MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(baos.toByteArray());
1027+
unpacker.unpackArrayHeader();
1028+
byte[] result = unpacker.readPayload(unpacker.unpackBinaryHeader());
1029+
assertArrayEquals(new byte[] {0x01, 0x02, 0x03}, result);
1030+
}
1031+
1032+
@Test
1033+
public void testWriteBinaryByteBufferWithOffset()
1034+
throws IOException
1035+
{
1036+
byte[] data = new byte[] {0x00, 0x01, 0x02, 0x03, 0x04};
1037+
ByteBuffer bb = ByteBuffer.wrap(data, 1, 3); // position=1, limit=4, remaining=3
1038+
1039+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
1040+
JsonGenerator generator = factory.createGenerator(baos, JsonEncoding.UTF8);
1041+
generator.writeStartArray();
1042+
ObjectMapper mapper = new MessagePackMapper(factory);
1043+
mapper.writeValue(generator, bb);
1044+
generator.writeEndArray();
1045+
generator.close();
1046+
1047+
MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(baos.toByteArray());
1048+
unpacker.unpackArrayHeader();
1049+
byte[] result = unpacker.readPayload(unpacker.unpackBinaryHeader());
1050+
assertArrayEquals(new byte[] {0x01, 0x02, 0x03}, result);
1051+
}
9591052
}

plans/preexisting-issues.md

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
# Issues to address
2+
3+
## msgpack-jackson3-specific
4+
5+
### 1. `isClosed()` always returns false
6+
7+
**File:** `msgpack-jackson3/.../MessagePackGenerator.java` (close method)
8+
9+
`close()` cannot call `super.close()` because `GeneratorBase.close()` in Jackson 3
10+
closes the underlying output stream as a side effect, breaking tests that disable
11+
`AUTO_CLOSE_TARGET`. As a result `isClosed()` always returns false and callers can
12+
continue writing into a closed generator without getting the standard exception.
13+
14+
### 2. `MessagePackFactory.snapshot()` returns `this` — FIXED
15+
16+
**File:** `msgpack-jackson3/.../MessagePackFactory.java`
17+
18+
Fixed: `snapshot()` now delegates to `copy()`, and `rebuild()` is implemented via `MessagePackFactoryBuilder`.
19+
20+
### 3. Build: `msgpack-jackson3` fails to compile locally on Java < 17
21+
22+
`msgpack-jackson3` is in the root aggregate unconditionally. The CI works around
23+
this with a bash version check, but a developer running `./sbt test` locally on
24+
Java 8 or 11 gets a hard compilation failure. A cleaner build-level solution
25+
(conditional aggregate, toolchain support, or a separate profile) is needed.
26+
27+
### 4. `MessagePackGenerator.streamWriteContext()` returns null
28+
29+
**File:** `msgpack-jackson3/.../MessagePackGenerator.java` (streamWriteContext method)
30+
31+
`streamWriteContext()` returns `null`, bypassing Jackson's standard write context
32+
management. This can cause NPEs in Jackson code paths that use the context for path
33+
tracking in error messages or certain serialization features. Fixing it properly
34+
requires integrating with Jackson 3's `TokenStreamContext` / `_streamWriteContext`
35+
managed by `GeneratorBase`, which needs investigation.
36+
37+
### 5. `writeString(Reader, int)` len=-1 implementation allocates an extra copy
38+
39+
**File:** `msgpack-jackson3/.../MessagePackGenerator.java` (writeString(Reader, int))
40+
41+
The len=-1 path buffers into a `StringBuilder` then copies to a `char[]`. Using a
42+
`CharArrayWriter` would avoid the intermediate allocation.
43+
44+
---
45+
46+
## msgpack-jackson pre-existing issues
47+
48+
These issues were identified during review of msgpack-jackson3 and confirmed to exist
49+
identically in msgpack-jackson. They should be addressed in both modules together.
50+
51+
## 1. `MessagePackParser`: Same byte-array input skips unpacker reset
52+
53+
**File:** `msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java:130`
54+
55+
When `AUTO_CLOSE_SOURCE` is disabled and the same byte-array instance is parsed more
56+
than once (e.g. reused buffer), the condition `messageUnpackerTuple.first() != src`
57+
is false, so the unpacker is not reset. The second parse continues from where the first
58+
left off instead of from the beginning.
59+
60+
## 2. `MessagePackParser`: ThreadLocal retains last byte-array payload per thread
61+
62+
**File:** `msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java:135`
63+
64+
`messageUnpackerHolder` is never cleared on parser close. For byte-array inputs this
65+
retains the entire last parsed payload for each thread in a pool indefinitely, which
66+
can cause unbounded memory retention after large messages.
67+
68+
## 3. `MessagePackGenerator`: `close()` does not call `super.close()`
69+
70+
**File:** `msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java` (close method)
71+
72+
The `close()` override never delegates to `GeneratorBase.close()`, so `isClosed()`
73+
remains false after close. Callers can continue writing into a closed generator
74+
instead of getting the standard closed-generator exception.
75+
76+
## 4. `MessagePackGenerator`: `writeString(Reader, int)` crashes on length -1
77+
78+
**File:** `msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java` (writeString(Reader, int))
79+
80+
`new char[len]` throws `NegativeArraySizeException` when `len` is -1, which is a
81+
valid Jackson API usage meaning "unknown length, read until EOF".
82+
83+
## 5. `MessagePackGenerator`: `writeNumber(String)` tries `Double.parseDouble` before `BigInteger`
84+
85+
**File:** `msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java:699`
86+
87+
Integer strings outside the `long` range are serialized as floating-point values,
88+
losing precision, even though MessagePack can encode big integers exactly. The order
89+
should try integer parsing first.
90+
91+
## 6. `MessagePackGenerator`: Closing a container leaves stale `currentState`
92+
93+
**File:** `msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java` (writeEndArray/writeEndObject)
94+
95+
After `flush()` clears `nodes`, any subsequent root-level value written with the
96+
same generator is treated as if inside the old container, which can corrupt output.
97+
98+
## 7. `MessagePackSerializedString`: Most interface methods are stubs
99+
100+
**File:** `msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializedString.java:70`
101+
102+
Most `SerializableString` methods return 0 or do nothing. Any Jackson code path
103+
that calls these methods (e.g. for length or byte-copy operations) will silently
104+
produce incorrect results.
105+
106+
## 8. `MessagePackGenerator`: `getBytesIfAscii` writes to wrong index when offset > 0
107+
108+
**Files:**
109+
- `msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java:474`
110+
- `msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java` — FIXED
111+
112+
`bytes[i] = (byte) c` should be `bytes[i - offset] = (byte) c`. When offset > 0, `i`
113+
starts above 0 but `bytes` is length `len`, so it throws `ArrayIndexOutOfBoundsException`.
114+
Fixed in msgpack-jackson3; needs the same fix in msgpack-jackson.
115+
116+
**Practical impact:** Low. `writeString(char[], offset, len)` is a low-level Jackson
117+
streaming API used by Jackson's own internals and performance-sensitive custom serializers,
118+
not by typical application code. Normal users call `writeString(String)` instead.
119+
120+
## 9. `MessagePackGenerator`: `writeByteArrayTextValue` ASCII path ignores offset
121+
122+
**Files:**
123+
- `msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java:512`
124+
- `msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java` — FIXED
125+
126+
`addValueNode(new AsciiCharString(text))` stores the entire backing array instead of
127+
the requested slice `[offset, offset+len)`. Callers such as `writeRawUTF8String` and
128+
`writeUTF8String` with non-zero offsets will serialize garbage bytes.
129+
Fixed in msgpack-jackson3 using `System.arraycopy`; needs the same fix in msgpack-jackson.
130+
131+
**Practical impact:** Low. `writeUTF8String(byte[], offset, len)` is a low-level API
132+
called by Jackson's streaming infrastructure or custom serializers working with raw
133+
byte buffers. Typical application code goes through ObjectMapper, which always passes
134+
offset=0 for plain byte arrays.
135+
136+
## 10. `MessagePackGenerator`: ByteBuffer serialization ignores `position()`
137+
138+
**Files:**
139+
- `msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java:369`
140+
- `msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java` — FIXED
141+
142+
`writePayload(bb.array(), bb.arrayOffset(), len)` ignores `bb.position()`. For any
143+
ByteBuffer with a non-zero position (e.g. from `ByteBuffer.wrap(data, offset, len)` or
144+
after reads), this serializes bytes starting at the wrong offset.
145+
Fixed in msgpack-jackson3 using `bb.arrayOffset() + bb.position()`; needs the same fix in msgpack-jackson.
146+
147+
**Practical impact:** Moderate. This is the most realistic end-user scenario: a POJO
148+
with a `ByteBuffer` field that was sliced or partially consumed will silently produce
149+
corrupt serialized output. No exception is thrown.
150+

0 commit comments

Comments
 (0)