Skip to content

Commit f33d5cc

Browse files
committed
decimal: 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 44db92b commit f33d5cc

File tree

6 files changed

+68
-31
lines changed

6 files changed

+68
-31
lines changed

CHANGELOG.md

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

4848
- Flaky decimal/TestSelect (#300)
49+
- Incorrect decoding of an MP_DECIMAL when the `scale` value is negative (#314)
4950

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

decimal/bcd.go

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

193193
// Get a BCD buffer without scale.
194194
bcdBuf = bcdBuf[scaleIdx+1:]
@@ -206,10 +206,6 @@ func decodeStringFromBCD(bcdBuf []byte) (string, error) {
206206

207207
// Reserve bytes for dot and sign.
208208
numLen := ndigits + 2
209-
// Reserve bytes for zeroes.
210-
if scale >= ndigits {
211-
numLen += scale - ndigits
212-
}
213209

214210
var bld strings.Builder
215211
bld.Grow(numLen)
@@ -221,26 +217,10 @@ func decodeStringFromBCD(bcdBuf []byte) (string, error) {
221217
bld.WriteByte('-')
222218
}
223219

224-
// Add missing zeroes to the left side when scale is bigger than a
225-
// number of digits and a single missed zero to the right side when
226-
// equal.
227-
if scale > ndigits {
228-
bld.WriteByte('0')
229-
bld.WriteByte('.')
230-
for diff := scale - ndigits; diff > 0; diff-- {
231-
bld.WriteByte('0')
232-
}
233-
} else if scale == ndigits {
234-
bld.WriteByte('0')
235-
}
236-
237220
const MaxDigit = 0x09
238221
// Builds a buffer with symbols of decimal number (digits, dot and sign).
239222
processNibble := func(nibble byte) {
240223
if nibble <= MaxDigit {
241-
if ndigits == scale {
242-
bld.WriteByte('.')
243-
}
244224
bld.WriteByte(nibble + '0')
245225
ndigits--
246226
}
@@ -256,5 +236,8 @@ func decodeStringFromBCD(bcdBuf []byte) (string, error) {
256236
processNibble(lowNibble)
257237
}
258238

259-
return bld.String(), nil
239+
if bld.Len() == 0 || isNegative[sign] && bld.Len() == 1 {
240+
bld.WriteByte('0')
241+
}
242+
return bld.String(), -1 * scale, nil
260243
}

decimal/decimal.go

+6-2
Original file line numberDiff line numberDiff line change
@@ -98,16 +98,20 @@ func (decNum *Decimal) UnmarshalMsgpack(b []byte) error {
9898
// +--------+-------------------+------------+===============+
9999
// | MP_EXT | length (optional) | MP_DECIMAL | PackedDecimal |
100100
// +--------+-------------------+------------+===============+
101-
digits, err := decodeStringFromBCD(b)
101+
digits, exp, err := decodeStringFromBCD(b)
102102
if err != nil {
103103
return fmt.Errorf("msgpack: can't decode string from BCD buffer (%x): %w", b, err)
104104
}
105+
105106
dec, err := decimal.NewFromString(digits)
106-
*decNum = *NewDecimal(dec)
107107
if err != nil {
108108
return fmt.Errorf("msgpack: can't encode string (%s) to a decimal number: %w", digits, err)
109109
}
110110

111+
if exp != 0 {
112+
dec = dec.Shift(int32(exp))
113+
}
114+
*decNum = *NewDecimal(dec)
111115
return nil
112116
}
113117

decimal/decimal_test.go

+47-1
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,17 @@ var correctnessSamples = []struct {
121121
"c7150113012345678912345678900987654321987654321d", false},
122122
}
123123

124+
var correctnessDecodeSamples = []struct {
125+
numString string
126+
mpBuf string
127+
fixExt bool
128+
}{
129+
{"1e2", "d501fe1c", true},
130+
{"1.1e31", "c70301e2011c", false},
131+
{"13e-2", "c7030102013c", false},
132+
{"-1e3", "d501fd1d", true},
133+
}
134+
124135
// There is a difference between encoding result from a raw string and from
125136
// decimal.Decimal. It's expected because decimal.Decimal simplifies decimals:
126137
// 0.00010000 -> 0.0001
@@ -397,18 +408,22 @@ func TestEncodeStringToBCD(t *testing.T) {
397408

398409
func TestDecodeStringFromBCD(t *testing.T) {
399410
samples := correctnessSamples
411+
samples = append(samples, correctnessDecodeSamples...)
400412
samples = append(samples, rawSamples...)
401413
samples = append(samples, benchmarkSamples...)
402414
for _, testcase := range samples {
403415
t.Run(testcase.numString, func(t *testing.T) {
404416
b, _ := hex.DecodeString(testcase.mpBuf)
405417
bcdBuf := trimMPHeader(b, testcase.fixExt)
406-
s, err := DecodeStringFromBCD(bcdBuf)
418+
s, exp, err := DecodeStringFromBCD(bcdBuf)
407419
if err != nil {
408420
t.Fatalf("Failed to decode BCD '%x' to decimal: %s", bcdBuf, err)
409421
}
410422

411423
decActual, err := decimal.NewFromString(s)
424+
if exp != 0 {
425+
decActual = decActual.Shift(int32(exp))
426+
}
412427
if err != nil {
413428
t.Fatalf("Failed to msgpack.Encoder string ('%s') to decimal", s)
414429
}
@@ -551,6 +566,37 @@ func TestSelect(t *testing.T) {
551566
tupleValueIsDecimal(t, resp.Data, number)
552567
}
553568

569+
func TestUnmarshal_from_decimal_new(t *testing.T) {
570+
skipIfDecimalUnsupported(t)
571+
572+
conn := test_helpers.ConnectWithValidation(t, server, opts)
573+
defer conn.Close()
574+
575+
samples := correctnessSamples
576+
samples = append(samples, correctnessDecodeSamples...)
577+
samples = append(samples, benchmarkSamples...)
578+
for _, testcase := range samples {
579+
str := testcase.numString
580+
t.Run(str, func(t *testing.T) {
581+
number, err := decimal.NewFromString(str)
582+
if err != nil {
583+
t.Fatalf("Failed to prepare test decimal: %s", err)
584+
}
585+
586+
call := NewEvalRequest("return require('decimal').new(...)").
587+
Args([]interface{}{str})
588+
resp, err := conn.Do(call).Get()
589+
if err != nil {
590+
t.Fatalf("Decimal create failed: %s", err)
591+
}
592+
if resp == nil {
593+
t.Fatalf("Response is nil after Call")
594+
}
595+
tupleValueIsDecimal(t, []interface{}{resp.Data}, number)
596+
})
597+
}
598+
}
599+
554600
func assertInsert(t *testing.T, conn *Connection, numString string) {
555601
number, err := decimal.NewFromString(numString)
556602
if err != nil {

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)