Skip to content

Commit d44ecef

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 (cherry picked from 3aeb8c2)
1 parent a756468 commit d44ecef

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
@@ -20,6 +20,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release.
2020

2121
- Flaky decimal/TestSelect (#300)
2222
- Race condition at roundRobinStrategy.GetNextConnection() (#309)
23+
- Incorrect decoding of an MP_DECIMAL when the `scale` value is negative (#314)
2324

2425
## [1.12.0] - 2023-06-07
2526

decimal/bcd.go

+17-27
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,11 @@ package decimal
4444
// * An implementation in C language https://github.com/tarantool/decNumber/blob/master/decPacked.c
4545

4646
import (
47+
"bytes"
4748
"fmt"
4849
"strings"
50+
51+
"github.com/vmihailenco/msgpack/v5"
4952
)
5053

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

191-
// Get a BCD buffer without scale.
192-
bcdBuf = bcdBuf[scaleIdx+1:]
198+
// Get the data without the scale.
199+
bcdBuf = buf.Bytes()
193200
bufLen := len(bcdBuf)
194201

195202
// Every nibble contains a digit, and the last low nibble contains a
@@ -204,10 +211,6 @@ func decodeStringFromBCD(bcdBuf []byte) (string, error) {
204211

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

212215
var bld strings.Builder
213216
bld.Grow(numLen)
@@ -219,26 +222,10 @@ func decodeStringFromBCD(bcdBuf []byte) (string, error) {
219222
bld.WriteByte('-')
220223
}
221224

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-
235225
const MaxDigit = 0x09
236226
// Builds a buffer with symbols of decimal number (digits, dot and sign).
237227
processNibble := func(nibble byte) {
238228
if nibble <= MaxDigit {
239-
if ndigits == scale {
240-
bld.WriteByte('.')
241-
}
242229
bld.WriteByte(nibble + '0')
243230
ndigits--
244231
}
@@ -254,5 +241,8 @@ func decodeStringFromBCD(bcdBuf []byte) (string, error) {
254241
processNibble(lowNibble)
255242
}
256243

257-
return bld.String(), nil
244+
if bld.Len() == 0 || isNegative[sign] && bld.Len() == 1 {
245+
bld.WriteByte('0')
246+
}
247+
return bld.String(), -1 * scale, nil
258248
}

decimal/decimal.go

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

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

decimal/decimal_test.go

+48-1
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,18 @@ var correctnessSamples = []struct {
112112
{"-1234567891234567890.0987654321987654321", "c7150113012345678912345678900987654321987654321d", false},
113113
}
114114

115+
var correctnessDecodeSamples = []struct {
116+
numString string
117+
mpBuf string
118+
fixExt bool
119+
}{
120+
{"1e2", "d501fe1c", true},
121+
{"1e33", "c70301d0df1c", false},
122+
{"1.1e31", "c70301e2011c", false},
123+
{"13e-2", "c7030102013c", false},
124+
{"-1e3", "d501fd1d", true},
125+
}
126+
115127
// There is a difference between encoding result from a raw string and from
116128
// decimal.Decimal. It's expected because decimal.Decimal simplifies decimals:
117129
// 0.00010000 -> 0.0001
@@ -384,17 +396,21 @@ func TestEncodeStringToBCD(t *testing.T) {
384396

385397
func TestDecodeStringFromBCD(t *testing.T) {
386398
samples := append(correctnessSamples, rawSamples...)
399+
samples = append(samples, correctnessDecodeSamples...)
387400
samples = append(samples, benchmarkSamples...)
388401
for _, testcase := range samples {
389402
t.Run(testcase.numString, func(t *testing.T) {
390403
b, _ := hex.DecodeString(testcase.mpBuf)
391404
bcdBuf := trimMPHeader(b, testcase.fixExt)
392-
s, err := DecodeStringFromBCD(bcdBuf)
405+
s, exp, err := DecodeStringFromBCD(bcdBuf)
393406
if err != nil {
394407
t.Fatalf("Failed to decode BCD '%x' to decimal: %s", bcdBuf, err)
395408
}
396409

397410
decActual, err := decimal.NewFromString(s)
411+
if exp != 0 {
412+
decActual = decActual.Shift(int32(exp))
413+
}
398414
if err != nil {
399415
t.Fatalf("Failed to encode string ('%s') to decimal", s)
400416
}
@@ -525,6 +541,37 @@ func TestSelect(t *testing.T) {
525541
tupleValueIsDecimal(t, resp.Data, number)
526542
}
527543

544+
func TestUnmarshal_from_decimal_new(t *testing.T) {
545+
skipIfDecimalUnsupported(t)
546+
547+
conn := test_helpers.ConnectWithValidation(t, server, opts)
548+
defer conn.Close()
549+
550+
samples := correctnessSamples
551+
samples = append(samples, correctnessDecodeSamples...)
552+
samples = append(samples, benchmarkSamples...)
553+
for _, testcase := range samples {
554+
str := testcase.numString
555+
t.Run(str, func(t *testing.T) {
556+
number, err := decimal.NewFromString(str)
557+
if err != nil {
558+
t.Fatalf("Failed to prepare test decimal: %s", err)
559+
}
560+
561+
call := NewEvalRequest("return require('decimal').new(...)").
562+
Args([]interface{}{str})
563+
resp, err := conn.Do(call).Get()
564+
if err != nil {
565+
t.Fatalf("Decimal create failed: %s", err)
566+
}
567+
if resp == nil {
568+
t.Fatalf("Response is nil after Call")
569+
}
570+
tupleValueIsDecimal(t, []interface{}{resp.Data}, number)
571+
})
572+
}
573+
}
574+
528575
func assertInsert(t *testing.T, conn *Connection, numString string) {
529576
number, err := decimal.NewFromString(numString)
530577
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/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)