Skip to content

Commit b67786f

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 dbfaab5 commit b67786f

File tree

6 files changed

+80
-35
lines changed

6 files changed

+80
-35
lines changed

CHANGELOG.md

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

4848
- Flaky decimal/TestSelect (#300)
4949
- Race condition at roundRobinStrategy.GetNextConnection() (#309)
50+
- Incorrect decoding of an MP_DECIMAL when the `scale` value is negative (#314)
5051

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

decimal/bcd.go

+17-27
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,11 @@ package decimal
4646
// https://github.com/tarantool/decNumber/blob/master/decPacked.c
4747

4848
import (
49+
"bytes"
4950
"fmt"
5051
"strings"
52+
53+
"github.com/vmihailenco/msgpack/v5"
5154
)
5255

5356
const (
@@ -185,13 +188,17 @@ func encodeStringToBCD(buf string) ([]byte, error) {
185188
// ended by a 4-bit sign nibble in the least significant four bits of the final
186189
// byte. The scale is used (negated) as the exponent of the decimal number.
187190
// Note that zeroes may have a sign and/or a scale.
188-
func decodeStringFromBCD(bcdBuf []byte) (string, error) {
189-
// Index of a byte with scale.
190-
const scaleIdx = 0
191-
scale := int(bcdBuf[scaleIdx])
191+
func decodeStringFromBCD(bcdBuf []byte) (string, int, error) {
192+
// Read scale.
193+
buf := bytes.NewBuffer(bcdBuf)
194+
dec := msgpack.NewDecoder(buf)
195+
scale, err := dec.DecodeInt()
196+
if err != nil {
197+
return "", 0, fmt.Errorf("unable to decode the decimal scale: %w", err)
198+
}
192199

193-
// Get a BCD buffer without scale.
194-
bcdBuf = bcdBuf[scaleIdx+1:]
200+
// Get the data without the scale.
201+
bcdBuf = buf.Bytes()
195202
bufLen := len(bcdBuf)
196203

197204
// Every nibble contains a digit, and the last low nibble contains a
@@ -206,10 +213,6 @@ func decodeStringFromBCD(bcdBuf []byte) (string, error) {
206213

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

214217
var bld strings.Builder
215218
bld.Grow(numLen)
@@ -221,26 +224,10 @@ func decodeStringFromBCD(bcdBuf []byte) (string, error) {
221224
bld.WriteByte('-')
222225
}
223226

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-
237227
const MaxDigit = 0x09
238228
// Builds a buffer with symbols of decimal number (digits, dot and sign).
239229
processNibble := func(nibble byte) {
240230
if nibble <= MaxDigit {
241-
if ndigits == scale {
242-
bld.WriteByte('.')
243-
}
244231
bld.WriteByte(nibble + '0')
245232
ndigits--
246233
}
@@ -256,5 +243,8 @@ func decodeStringFromBCD(bcdBuf []byte) (string, error) {
256243
processNibble(lowNibble)
257244
}
258245

259-
return bld.String(), nil
246+
if bld.Len() == 0 || isNegative[sign] && bld.Len() == 1 {
247+
bld.WriteByte('0')
248+
}
249+
return bld.String(), -1 * scale, nil
260250
}

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

+48-1
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,18 @@ 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+
{"1e33", "c70301d0df1c", false},
131+
{"1.1e31", "c70301e2011c", false},
132+
{"13e-2", "c7030102013c", false},
133+
{"-1e3", "d501fd1d", true},
134+
}
135+
124136
// There is a difference between encoding result from a raw string and from
125137
// decimal.Decimal. It's expected because decimal.Decimal simplifies decimals:
126138
// 0.00010000 -> 0.0001
@@ -397,18 +409,22 @@ func TestEncodeStringToBCD(t *testing.T) {
397409

398410
func TestDecodeStringFromBCD(t *testing.T) {
399411
samples := correctnessSamples
412+
samples = append(samples, correctnessDecodeSamples...)
400413
samples = append(samples, rawSamples...)
401414
samples = append(samples, benchmarkSamples...)
402415
for _, testcase := range samples {
403416
t.Run(testcase.numString, func(t *testing.T) {
404417
b, _ := hex.DecodeString(testcase.mpBuf)
405418
bcdBuf := trimMPHeader(b, testcase.fixExt)
406-
s, err := DecodeStringFromBCD(bcdBuf)
419+
s, exp, err := DecodeStringFromBCD(bcdBuf)
407420
if err != nil {
408421
t.Fatalf("Failed to decode BCD '%x' to decimal: %s", bcdBuf, err)
409422
}
410423

411424
decActual, err := decimal.NewFromString(s)
425+
if exp != 0 {
426+
decActual = decActual.Shift(int32(exp))
427+
}
412428
if err != nil {
413429
t.Fatalf("Failed to msgpack.Encoder string ('%s') to decimal", s)
414430
}
@@ -551,6 +567,37 @@ func TestSelect(t *testing.T) {
551567
tupleValueIsDecimal(t, resp.Data, number)
552568
}
553569

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