Skip to content

Commit

Permalink
xdr: Add a custom marshaller for claim predicate timestamp (#3183)
Browse files Browse the repository at this point in the history
Currently, claimable balances which have a AbsBefore timestamp beyond year 9999 causes Horizon ingestion to crash. The crash is due to the fact that the time.Parse() implementation does not support parsing strings that far into the future.
  • Loading branch information
tamirms authored Nov 2, 2020
1 parent 84a9946 commit aa35ab6
Show file tree
Hide file tree
Showing 2 changed files with 163 additions and 3 deletions.
60 changes: 57 additions & 3 deletions xdr/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,69 @@ package xdr
import (
"encoding/json"
"fmt"
"regexp"
"strconv"
"time"

"github.com/stellar/go/support/errors"
)

// iso8601Time is a timestamp which supports parsing dates which have a year outside the 0000..9999 range
type iso8601Time struct {
time.Time
}

// reISO8601 is the regular expression used to parse date strings in the
// ISO 8601 extended format, with or without an expanded year representation.
var reISO8601 = regexp.MustCompile(`^([-+]?\d{4,})-(\d{2})-(\d{2})`)

// MarshalJSON serializes the timestamp to a string
func (t iso8601Time) MarshalJSON() ([]byte, error) {
ts := t.Format(time.RFC3339)
if t.Year() > 9999 {
ts = "+" + ts
}

return json.Marshal(ts)
}

// UnmarshalJSON parses a JSON string into a iso8601Time instance.
func (t *iso8601Time) UnmarshalJSON(b []byte) error {
var s *string
if err := json.Unmarshal(b, &s); err != nil {
return err
}
if s == nil {
return nil
}

text := *s
m := reISO8601.FindStringSubmatch(text)

if len(m) != 4 {
return fmt.Errorf("UnmarshalJSON: cannot parse %s", text)
}
// No need to check for errors since the regexp guarantees the matches
// are valid integers
year, _ := strconv.Atoi(m[1])
month, _ := strconv.Atoi(m[2])
day, _ := strconv.Atoi(m[3])

ts, err := time.Parse(time.RFC3339, "2006-01-02"+text[len(m[0]):])
if err != nil {
return errors.Wrap(err, "Could not extract time")
}

t.Time = time.Date(year, time.Month(month), day, ts.Hour(), ts.Minute(), ts.Second(), ts.Nanosecond(), ts.Location())
return nil
}

type claimPredicateJSON struct {
And *[]claimPredicateJSON `json:"and,omitempty"`
Or *[]claimPredicateJSON `json:"or,omitempty"`
Not *claimPredicateJSON `json:"not,omitempty"`
Unconditional bool `json:"unconditional,omitempty"`
AbsBefore *time.Time `json:"abs_before,omitempty"`
AbsBefore *iso8601Time `json:"abs_before,omitempty"`
RelBefore *int64 `json:"rel_before,string,omitempty"`
}

Expand Down Expand Up @@ -98,8 +152,8 @@ func (c ClaimPredicate) toJSON() (claimPredicateJSON, error) {
payload.Not = new(claimPredicateJSON)
*payload.Not, err = c.MustNotPredicate().toJSON()
case ClaimPredicateTypeClaimPredicateBeforeAbsoluteTime:
payload.AbsBefore = new(time.Time)
*payload.AbsBefore = time.Unix(int64(c.MustAbsBefore()), 0).UTC()
payload.AbsBefore = new(iso8601Time)
*payload.AbsBefore = iso8601Time{time.Unix(int64(c.MustAbsBefore()), 0).UTC()}
case ClaimPredicateTypeClaimPredicateBeforeRelativeTime:
payload.RelBefore = new(int64)
*payload.RelBefore = int64(c.MustRelBefore())
Expand Down
106 changes: 106 additions & 0 deletions xdr/json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package xdr

import (
"encoding/json"
"math"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -57,3 +58,108 @@ func TestClaimPredicateJSON(t *testing.T) {

assert.Equal(t, serializedBase64, parsedBase64)
}

func TestAbsBeforeTimestamps(t *testing.T) {
const year = 365 * 24 * 60 * 60
for _, testCase := range []struct {
unix int64
expected string
}{
{
0,
`{"abs_before":"1970-01-01T00:00:00Z"}`,
},
{
900 * year,
`{"abs_before":"2869-05-27T00:00:00Z"}`,
},
{
math.MaxInt64,
`{"abs_before":"+292277026596-12-04T15:30:07Z"}`,
},
{
-10,
`{"abs_before":"1969-12-31T23:59:50Z"}`,
},
{
-9000 * year,
`{"abs_before":"-7025-12-23T00:00:00Z"}`,
},
{
math.MinInt64,
// this serialization doesn't make sense but at least it doesn't crash the marshaller
`{"abs_before":"+292277026596-12-04T15:30:08Z"}`,
},
} {
xdrSec := Int64(testCase.unix)
source := ClaimPredicate{
Type: ClaimPredicateTypeClaimPredicateBeforeAbsoluteTime,
AbsBefore: &xdrSec,
}

serialized, err := json.Marshal(source)
assert.NoError(t, err)
assert.JSONEq(t, testCase.expected, string(serialized))

var parsed ClaimPredicate
assert.NoError(t, json.Unmarshal(serialized, &parsed))
assert.Equal(t, *parsed.AbsBefore, *source.AbsBefore)
}
}

func TestISO8601Time_UnmarshalJSON(t *testing.T) {
for _, testCase := range []struct {
name string
timestamp string
expectedParsed iso8601Time
expectedError string
}{
{
"null timestamp",
"null",
iso8601Time{},
"",
},
{
"empty string",
"",
iso8601Time{},
"unexpected end of JSON input",
},
{
"not string",
"1",
iso8601Time{},
"json: cannot unmarshal number into Go value of type string",
},
{
"does not begin with double quotes",
"'1\"",
iso8601Time{},
"invalid character '\\'' looking for beginning of value",
},
{
"does not end with double quotes",
"\"1",
iso8601Time{},
"unexpected end of JSON input",
},
{
"could not extract time",
"\"2006-01-02aldfd\"",
iso8601Time{},
"Could not extract time: parsing time \"2006-01-02aldfd\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"aldfd\" as \"T\"",
},
} {
t.Run(testCase.name, func(t *testing.T) {
ts := &iso8601Time{}
err := ts.UnmarshalJSON([]byte(testCase.timestamp))
if len(testCase.expectedError) == 0 {
assert.NoError(t, err)
assert.Equal(t, *ts, testCase.expectedParsed)
} else {
assert.EqualError(t, err, testCase.expectedError)
}
})
}
}

0 comments on commit aa35ab6

Please sign in to comment.