Four correctness fixes. The first affects every platform; the other three are
web-only (dart2js/dartdevc), where an int is a JavaScript double and the
bitwise operators are 32-bit.
-
DataSerializerPlatformGeneric(web):- Fixed
shiftLeftIntandshiftRightIntfor non-negative operands.
Both took a plain<</>>fast path, which is a 32-bit operation here:shiftLeftInt(127, 28)returned4026531840instead of34091302912,
and any shift of 32 or more returned0.shiftRightInttruncated its operand before shifting, so
shiftRightInt(9007199254740991, 7)returned33554431instead of
70368744177663.- Both now fall back to [BigInt] outside the range the 32-bit operators
handle exactly. Negative operands already did.
- This corrupted LEB128 encoding and decoding on the web for magnitudes past
2^28.
- Fixed
-
Leb128/BytesBufferLeb128Extension(web):- Fixed the value accumulator, which was truncating at 32 bits. The
decoders folded each 7-bit group in with|=, and|is also a 32-bit
operation on the web, so any value past 2^32 lost its high bits. The groups
occupy disjoint bit ranges, so+is equivalent and stays exact. - Fixed signed decoding for magnitudes past ~2^49. A LEB128 negative
sign-extends into every bit below its terminating byte, so the unsigned
form of a negative value is roughly2^shift— far larger than the value
itself, and past the range a JavaScript double represents exactly, even
when the value is comfortably inside it. The accumulator is now split at 49
bits and the halves recombined with [BigInt] only when the value reaches
that far.DateTime.microsecondsSinceEpochsits in the affected range
(about 2^50.6), so a pre-1970 timestamp decoded to the wrong number on the
web while looking entirely ordinary.
- Fixed the value accumulator, which was truncating at 32 bits. The
-
BytesBufferLeb128Extension:- Fixed
readLeb128SignedInt: it decoded almost every signed value
incorrectly, on every platform.- The sign of a LEB128 value lives in bit 6 of its terminating byte, but
lastBytewas assigned after thebreak, so it only ever held the
previous continuation byte — or0for a single-byte value. - Every negative value came back positive:
-1read as127,-2as
126,-64as64. - Multi-byte positives were affected in the other direction:
64encodes as
[0xC0, 0x00], and reading the sign from0xC0turned it into-16320;
1000became-15384. - Values whose terminating and preceding bytes happened to agree on bit 6
decoded correctly by luck, which is why the existing-1000000test
passed throughout. - The static
Leb128.decodeSignedwas never affected — it reads the sign
from the correct byte — so encoding was always right and only the
BytesBufferread path was wrong.
- The sign of a LEB128 value lives in bit 6 of its terminating byte, but
- Fixed
-
Tests:
- Added regression tests for
readLeb128SignedInt: single-byte negatives,
multi-byte positives, a wide range of magnitudes, interleaved signed and
unsigned reads in one buffer, and a cross-check pinning the buffer reader to
Leb128.decodeSignedso a fix on one side cannot be undone on the other. - Added range tests for signed and unsigned reads from 2^28 up to 2^53-1, and
a case at microsecond-timestamp magnitudes, which is where the signed
accumulator went wrong on the web. - Added direct tests for
shiftLeftIntandshiftRightIntacross and beyond
the 32-bit boundary, checked againstBigInt, for positive and negative
operands. - Ranges are built by multiplication rather than
1 << 40, since a shift is a
32-bit operation on the web and such a literal is silently0there — a
test written that way does not check what it appears to.
- Added regression tests for
-
CI:
.github/workflows/dart.yml: the Chrome job now collects and uploads
coverage too. The two platforms run different implementations —
platform_generic.dartis the web one and is never loaded on the VM — so a
VM-only report left every web-only line permanently unmeasured, whatever
the tests actually did. That is precisely where three of the four bugs
above were hiding.- Added
data_serializer_edge_cases_test.dartcovering paths the suite had
not reached:BytesEmitter's rejection of data types it cannot represent,
BytesBufferError.toString, theBytesIOtransfer defaults, the
ByteDataExtensionlength-range checks,DataSerializerPlatform's 64-bit
reads,BitsBuffer.isAtPadding/toString,Writable's default buffer
size, andBytesFileIO's public constructor, transfer defaults and capacity
growth. - Coverage: 98.1% → 99.6%.