Skip to content

Commit a70cae2

Browse files
committed
bugfix: incorrect MP_DECIMAL decoding with scale < 0
The `scale` value in `MP_DECIMAL` may be negative [1]. We need to handle the case. 1. https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-decimal-type
1 parent c2498be commit a70cae2

File tree

6 files changed

+39
-32
lines changed

6 files changed

+39
-32
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release.
4545
### Fixed
4646

4747
- Flaky decimal/TestSelect (#300)
48+
- Incorrect decoding of an MP_DECIMAL when the `scale` value is negative (#314)
4849

4950
## [1.12.0] - 2023-06-07
5051

decimal/bcd.go

+6-23
Original file line numberDiff line numberDiff line change
@@ -183,10 +183,10 @@ func encodeStringToBCD(buf string) ([]byte, error) {
183183
// ended by a 4-bit sign nibble in the least significant four bits of the final
184184
// byte. The scale is used (negated) as the exponent of the decimal number.
185185
// Note that zeroes may have a sign and/or a scale.
186-
func decodeStringFromBCD(bcdBuf []byte) (string, error) {
186+
func decodeStringFromBCD(bcdBuf []byte) (string, int, error) {
187187
// Index of a byte with scale.
188188
const scaleIdx = 0
189-
scale := int(bcdBuf[scaleIdx])
189+
scale := int(int8(bcdBuf[scaleIdx]))
190190

191191
// Get a BCD buffer without scale.
192192
bcdBuf = bcdBuf[scaleIdx+1:]
@@ -204,10 +204,6 @@ func decodeStringFromBCD(bcdBuf []byte) (string, error) {
204204

205205
// Reserve bytes for dot and sign.
206206
numLen := ndigits + 2
207-
// Reserve bytes for zeroes.
208-
if scale >= ndigits {
209-
numLen += scale - ndigits
210-
}
211207

212208
var bld strings.Builder
213209
bld.Grow(numLen)
@@ -219,26 +215,10 @@ func decodeStringFromBCD(bcdBuf []byte) (string, error) {
219215
bld.WriteByte('-')
220216
}
221217

222-
// Add missing zeroes to the left side when scale is bigger than a
223-
// number of digits and a single missed zero to the right side when
224-
// equal.
225-
if scale > ndigits {
226-
bld.WriteByte('0')
227-
bld.WriteByte('.')
228-
for diff := scale - ndigits; diff > 0; diff-- {
229-
bld.WriteByte('0')
230-
}
231-
} else if scale == ndigits {
232-
bld.WriteByte('0')
233-
}
234-
235218
const MaxDigit = 0x09
236219
// Builds a buffer with symbols of decimal number (digits, dot and sign).
237220
processNibble := func(nibble byte) {
238221
if nibble <= MaxDigit {
239-
if ndigits == scale {
240-
bld.WriteByte('.')
241-
}
242222
bld.WriteByte(nibble + '0')
243223
ndigits--
244224
}
@@ -254,5 +234,8 @@ func decodeStringFromBCD(bcdBuf []byte) (string, error) {
254234
processNibble(lowNibble)
255235
}
256236

257-
return bld.String(), nil
237+
if bld.Len() == 0 {
238+
bld.WriteByte('0')
239+
}
240+
return bld.String(), -1 * scale, nil
258241
}

decimal/decimal.go

+6-2
Original file line numberDiff line numberDiff line change
@@ -87,16 +87,20 @@ func (decNum *Decimal) UnmarshalMsgpack(b []byte) error {
8787
// +--------+-------------------+------------+===============+
8888
// | MP_EXT | length (optional) | MP_DECIMAL | PackedDecimal |
8989
// +--------+-------------------+------------+===============+
90-
digits, err := decodeStringFromBCD(b)
90+
digits, exp, err := decodeStringFromBCD(b)
9191
if err != nil {
9292
return fmt.Errorf("msgpack: can't decode string from BCD buffer (%x): %w", b, err)
9393
}
94+
9495
dec, err := decimal.NewFromString(digits)
95-
*decNum = *NewDecimal(dec)
9696
if err != nil {
9797
return fmt.Errorf("msgpack: can't encode string (%s) to a decimal number: %w", digits, err)
9898
}
9999

100+
if exp != 0 {
101+
dec = dec.Shift(int32(exp))
102+
}
103+
*decNum = *NewDecimal(dec)
100104
return nil
101105
}
102106

decimal/decimal_test.go

+18-2
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,17 @@ var correctnessSamples = []struct {
116116
{"-1234567891234567890.0987654321987654321", "c7150113012345678912345678900987654321987654321d", false},
117117
}
118118

119+
var correctnessDecodeSamples = []struct {
120+
numString string
121+
mpBuf string
122+
fixExt bool
123+
}{
124+
{"1e2", "d501fe1c", true},
125+
{"1.1e31", "c70301e2011c", false},
126+
{"13e-2", "c7030102013c", false},
127+
{"-1e3", "d501fd1d", true},
128+
}
129+
119130
// There is a difference between encoding result from a raw string and from
120131
// decimal.Decimal. It's expected because decimal.Decimal simplifies decimals:
121132
// 0.00010000 -> 0.0001
@@ -387,18 +398,23 @@ func TestEncodeStringToBCD(t *testing.T) {
387398
}
388399

389400
func TestDecodeStringFromBCD(t *testing.T) {
390-
samples := append(correctnessSamples, rawSamples...)
401+
samples := correctnessSamples
402+
samples = append(samples, correctnessDecodeSamples...)
403+
samples = append(samples, rawSamples...)
391404
samples = append(samples, benchmarkSamples...)
392405
for _, testcase := range samples {
393406
t.Run(testcase.numString, func(t *testing.T) {
394407
b, _ := hex.DecodeString(testcase.mpBuf)
395408
bcdBuf := trimMPHeader(b, testcase.fixExt)
396-
s, err := DecodeStringFromBCD(bcdBuf)
409+
s, exp, err := DecodeStringFromBCD(bcdBuf)
397410
if err != nil {
398411
t.Fatalf("Failed to decode BCD '%x' to decimal: %s", bcdBuf, err)
399412
}
400413

401414
decActual, err := decimal.NewFromString(s)
415+
if exp != 0 {
416+
decActual = decActual.Shift(int32(exp))
417+
}
402418
if err != nil {
403419
t.Fatalf("Failed to msgpack.Encoder string ('%s') to decimal", s)
404420
}

decimal/export_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ func EncodeStringToBCD(buf string) ([]byte, error) {
44
return encodeStringToBCD(buf)
55
}
66

7-
func DecodeStringFromBCD(bcdBuf []byte) (string, error) {
7+
func DecodeStringFromBCD(bcdBuf []byte) (string, int, error) {
88
return decodeStringFromBCD(bcdBuf)
99
}
1010

decimal/fuzzing_test.go

+7-4
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,14 @@ import (
1010
. "github.com/tarantool/go-tarantool/v2/decimal"
1111
)
1212

13-
func strToDecimal(t *testing.T, buf string) decimal.Decimal {
13+
func strToDecimal(t *testing.T, buf string, exp int) decimal.Decimal {
1414
decNum, err := decimal.NewFromString(buf)
1515
if err != nil {
1616
t.Fatal(err)
1717
}
18+
if exp != 0 {
19+
decNum = decNum.Shift(int32(exp))
20+
}
1821
return decNum
1922
}
2023

@@ -33,13 +36,13 @@ func FuzzEncodeDecodeBCD(f *testing.F) {
3336
if err != nil {
3437
t.Skip("Only correct requests are interesting: %w", err)
3538
}
36-
var dec string
37-
dec, err = DecodeStringFromBCD(bcdBuf)
39+
40+
dec, exp, err := DecodeStringFromBCD(bcdBuf)
3841
if err != nil {
3942
t.Fatalf("Failed to decode encoded value ('%s')", orig)
4043
}
4144

42-
if !strToDecimal(t, dec).Equal(strToDecimal(t, orig)) {
45+
if !strToDecimal(t, dec, exp).Equal(strToDecimal(t, orig, 0)) {
4346
t.Fatal("Decimal numbers are not equal")
4447
}
4548
})

0 commit comments

Comments
 (0)