5cbdea6192
According to RFC3339 section 5.6, the maximum time offset values for hours and minutes is 23 and 59, respectively.
528 lines
12 KiB
Go
528 lines
12 KiB
Go
package toml
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"strconv"
|
|
"time"
|
|
)
|
|
|
|
func parseInteger(b []byte) (int64, error) {
|
|
if len(b) > 2 && b[0] == '0' {
|
|
switch b[1] {
|
|
case 'x':
|
|
return parseIntHex(b)
|
|
case 'b':
|
|
return parseIntBin(b)
|
|
case 'o':
|
|
return parseIntOct(b)
|
|
default:
|
|
panic(fmt.Errorf("invalid base '%c', should have been checked by scanIntOrFloat", b[1]))
|
|
}
|
|
}
|
|
|
|
return parseIntDec(b)
|
|
}
|
|
|
|
func parseLocalDate(b []byte) (LocalDate, error) {
|
|
// full-date = date-fullyear "-" date-month "-" date-mday
|
|
// date-fullyear = 4DIGIT
|
|
// date-month = 2DIGIT ; 01-12
|
|
// date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on month/year
|
|
var date LocalDate
|
|
|
|
if len(b) != 10 || b[4] != '-' || b[7] != '-' {
|
|
return date, newDecodeError(b, "dates are expected to have the format YYYY-MM-DD")
|
|
}
|
|
|
|
var err error
|
|
|
|
date.Year, err = parseDecimalDigits(b[0:4])
|
|
if err != nil {
|
|
return LocalDate{}, err
|
|
}
|
|
|
|
date.Month, err = parseDecimalDigits(b[5:7])
|
|
if err != nil {
|
|
return LocalDate{}, err
|
|
}
|
|
|
|
date.Day, err = parseDecimalDigits(b[8:10])
|
|
if err != nil {
|
|
return LocalDate{}, err
|
|
}
|
|
|
|
if !isValidDate(date.Year, date.Month, date.Day) {
|
|
return LocalDate{}, newDecodeError(b, "impossible date")
|
|
}
|
|
|
|
return date, nil
|
|
}
|
|
|
|
func parseDecimalDigits(b []byte) (int, error) {
|
|
v := 0
|
|
|
|
for i, c := range b {
|
|
if c < '0' || c > '9' {
|
|
return 0, newDecodeError(b[i:i+1], "expected digit (0-9)")
|
|
}
|
|
v *= 10
|
|
v += int(c - '0')
|
|
}
|
|
|
|
return v, nil
|
|
}
|
|
|
|
func parseDateTime(b []byte) (time.Time, error) {
|
|
// offset-date-time = full-date time-delim full-time
|
|
// full-time = partial-time time-offset
|
|
// time-offset = "Z" / time-numoffset
|
|
// time-numoffset = ( "+" / "-" ) time-hour ":" time-minute
|
|
|
|
dt, b, err := parseLocalDateTime(b)
|
|
if err != nil {
|
|
return time.Time{}, err
|
|
}
|
|
|
|
var zone *time.Location
|
|
|
|
if len(b) == 0 {
|
|
// parser should have checked that when assigning the date time node
|
|
panic("date time should have a timezone")
|
|
}
|
|
|
|
if b[0] == 'Z' || b[0] == 'z' {
|
|
b = b[1:]
|
|
zone = time.UTC
|
|
} else {
|
|
const dateTimeByteLen = 6
|
|
if len(b) != dateTimeByteLen {
|
|
return time.Time{}, newDecodeError(b, "invalid date-time timezone")
|
|
}
|
|
var direction int
|
|
switch b[0] {
|
|
case '-':
|
|
direction = -1
|
|
case '+':
|
|
direction = +1
|
|
default:
|
|
return time.Time{}, newDecodeError(b[:1], "invalid timezone offset character")
|
|
}
|
|
|
|
if b[3] != ':' {
|
|
return time.Time{}, newDecodeError(b[3:4], "expected a : separator")
|
|
}
|
|
|
|
hours, err := parseDecimalDigits(b[1:3])
|
|
if err != nil {
|
|
return time.Time{}, err
|
|
}
|
|
if hours > 23 {
|
|
return time.Time{}, newDecodeError(b[:1], "invalid timezone offset hours")
|
|
}
|
|
|
|
minutes, err := parseDecimalDigits(b[4:6])
|
|
if err != nil {
|
|
return time.Time{}, err
|
|
}
|
|
if minutes > 59 {
|
|
return time.Time{}, newDecodeError(b[:1], "invalid timezone offset minutes")
|
|
}
|
|
|
|
seconds := direction * (hours*3600 + minutes*60)
|
|
zone = time.FixedZone("", seconds)
|
|
b = b[dateTimeByteLen:]
|
|
}
|
|
|
|
if len(b) > 0 {
|
|
return time.Time{}, newDecodeError(b, "extra bytes at the end of the timezone")
|
|
}
|
|
|
|
t := time.Date(
|
|
dt.Year,
|
|
time.Month(dt.Month),
|
|
dt.Day,
|
|
dt.Hour,
|
|
dt.Minute,
|
|
dt.Second,
|
|
dt.Nanosecond,
|
|
zone)
|
|
|
|
return t, nil
|
|
}
|
|
|
|
func parseLocalDateTime(b []byte) (LocalDateTime, []byte, error) {
|
|
var dt LocalDateTime
|
|
|
|
const localDateTimeByteMinLen = 11
|
|
if len(b) < localDateTimeByteMinLen {
|
|
return dt, nil, newDecodeError(b, "local datetimes are expected to have the format YYYY-MM-DDTHH:MM:SS[.NNNNNNNNN]")
|
|
}
|
|
|
|
date, err := parseLocalDate(b[:10])
|
|
if err != nil {
|
|
return dt, nil, err
|
|
}
|
|
dt.LocalDate = date
|
|
|
|
sep := b[10]
|
|
if sep != 'T' && sep != ' ' && sep != 't' {
|
|
return dt, nil, newDecodeError(b[10:11], "datetime separator is expected to be T or a space")
|
|
}
|
|
|
|
t, rest, err := parseLocalTime(b[11:])
|
|
if err != nil {
|
|
return dt, nil, err
|
|
}
|
|
dt.LocalTime = t
|
|
|
|
return dt, rest, nil
|
|
}
|
|
|
|
// parseLocalTime is a bit different because it also returns the remaining
|
|
// []byte that is didn't need. This is to allow parseDateTime to parse those
|
|
// remaining bytes as a timezone.
|
|
func parseLocalTime(b []byte) (LocalTime, []byte, error) {
|
|
var (
|
|
nspow = [10]int{0, 1e8, 1e7, 1e6, 1e5, 1e4, 1e3, 1e2, 1e1, 1e0}
|
|
t LocalTime
|
|
)
|
|
|
|
// check if b matches to have expected format HH:MM:SS[.NNNNNN]
|
|
const localTimeByteLen = 8
|
|
if len(b) < localTimeByteLen {
|
|
return t, nil, newDecodeError(b, "times are expected to have the format HH:MM:SS[.NNNNNN]")
|
|
}
|
|
|
|
var err error
|
|
|
|
t.Hour, err = parseDecimalDigits(b[0:2])
|
|
if err != nil {
|
|
return t, nil, err
|
|
}
|
|
|
|
if t.Hour > 23 {
|
|
return t, nil, newDecodeError(b[0:2], "hour cannot be greater 23")
|
|
}
|
|
if b[2] != ':' {
|
|
return t, nil, newDecodeError(b[2:3], "expecting colon between hours and minutes")
|
|
}
|
|
|
|
t.Minute, err = parseDecimalDigits(b[3:5])
|
|
if err != nil {
|
|
return t, nil, err
|
|
}
|
|
if t.Minute > 59 {
|
|
return t, nil, newDecodeError(b[3:5], "minutes cannot be greater 59")
|
|
}
|
|
if b[5] != ':' {
|
|
return t, nil, newDecodeError(b[5:6], "expecting colon between minutes and seconds")
|
|
}
|
|
|
|
t.Second, err = parseDecimalDigits(b[6:8])
|
|
if err != nil {
|
|
return t, nil, err
|
|
}
|
|
|
|
if t.Second > 60 {
|
|
return t, nil, newDecodeError(b[6:8], "seconds cannot be greater 60")
|
|
}
|
|
|
|
b = b[8:]
|
|
|
|
if len(b) >= 1 && b[0] == '.' {
|
|
frac := 0
|
|
digits := 0
|
|
|
|
for i, c := range b[1:] {
|
|
if !isDigit(c) {
|
|
if i == 0 {
|
|
return t, nil, newDecodeError(b[0:1], "need at least one digit after fraction point")
|
|
}
|
|
break
|
|
}
|
|
|
|
const maxFracPrecision = 9
|
|
if i >= maxFracPrecision {
|
|
return t, nil, newDecodeError(b[i-1:i], "maximum precision for date time is nanosecond")
|
|
}
|
|
|
|
frac *= 10
|
|
frac += int(c - '0')
|
|
digits++
|
|
}
|
|
|
|
if digits == 0 {
|
|
return t, nil, newDecodeError(b[:1], "nanoseconds need at least one digit")
|
|
}
|
|
|
|
t.Nanosecond = frac * nspow[digits]
|
|
t.Precision = digits
|
|
|
|
return t, b[1+digits:], nil
|
|
}
|
|
return t, b, nil
|
|
}
|
|
|
|
//nolint:cyclop
|
|
func parseFloat(b []byte) (float64, error) {
|
|
if len(b) == 4 && (b[0] == '+' || b[0] == '-') && b[1] == 'n' && b[2] == 'a' && b[3] == 'n' {
|
|
return math.NaN(), nil
|
|
}
|
|
|
|
cleaned, err := checkAndRemoveUnderscoresFloats(b)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
if cleaned[0] == '.' {
|
|
return 0, newDecodeError(b, "float cannot start with a dot")
|
|
}
|
|
|
|
if cleaned[len(cleaned)-1] == '.' {
|
|
return 0, newDecodeError(b, "float cannot end with a dot")
|
|
}
|
|
|
|
dotAlreadySeen := false
|
|
for i, c := range cleaned {
|
|
if c == '.' {
|
|
if dotAlreadySeen {
|
|
return 0, newDecodeError(b[i:i+1], "float can have at most one decimal point")
|
|
}
|
|
if !isDigit(cleaned[i-1]) {
|
|
return 0, newDecodeError(b[i-1:i+1], "float decimal point must be preceded by a digit")
|
|
}
|
|
if !isDigit(cleaned[i+1]) {
|
|
return 0, newDecodeError(b[i:i+2], "float decimal point must be followed by a digit")
|
|
}
|
|
dotAlreadySeen = true
|
|
}
|
|
}
|
|
|
|
start := 0
|
|
if b[0] == '+' || b[0] == '-' {
|
|
start = 1
|
|
}
|
|
if b[start] == '0' && isDigit(b[start+1]) {
|
|
return 0, newDecodeError(b, "float integer part cannot have leading zeroes")
|
|
}
|
|
|
|
f, err := strconv.ParseFloat(string(cleaned), 64)
|
|
if err != nil {
|
|
return 0, newDecodeError(b, "unable to parse float: %w", err)
|
|
}
|
|
|
|
return f, nil
|
|
}
|
|
|
|
func parseIntHex(b []byte) (int64, error) {
|
|
cleaned, err := checkAndRemoveUnderscoresIntegers(b[2:])
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
i, err := strconv.ParseInt(string(cleaned), 16, 64)
|
|
if err != nil {
|
|
return 0, newDecodeError(b, "couldn't parse hexadecimal number: %w", err)
|
|
}
|
|
|
|
return i, nil
|
|
}
|
|
|
|
func parseIntOct(b []byte) (int64, error) {
|
|
cleaned, err := checkAndRemoveUnderscoresIntegers(b[2:])
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
i, err := strconv.ParseInt(string(cleaned), 8, 64)
|
|
if err != nil {
|
|
return 0, newDecodeError(b, "couldn't parse octal number: %w", err)
|
|
}
|
|
|
|
return i, nil
|
|
}
|
|
|
|
func parseIntBin(b []byte) (int64, error) {
|
|
cleaned, err := checkAndRemoveUnderscoresIntegers(b[2:])
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
i, err := strconv.ParseInt(string(cleaned), 2, 64)
|
|
if err != nil {
|
|
return 0, newDecodeError(b, "couldn't parse binary number: %w", err)
|
|
}
|
|
|
|
return i, nil
|
|
}
|
|
|
|
func isSign(b byte) bool {
|
|
return b == '+' || b == '-'
|
|
}
|
|
|
|
func parseIntDec(b []byte) (int64, error) {
|
|
cleaned, err := checkAndRemoveUnderscoresIntegers(b)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
startIdx := 0
|
|
|
|
if isSign(cleaned[0]) {
|
|
startIdx++
|
|
}
|
|
|
|
if len(cleaned) > startIdx+1 && cleaned[startIdx] == '0' {
|
|
return 0, newDecodeError(b, "leading zero not allowed on decimal number")
|
|
}
|
|
|
|
i, err := strconv.ParseInt(string(cleaned), 10, 64)
|
|
if err != nil {
|
|
return 0, newDecodeError(b, "couldn't parse decimal number: %w", err)
|
|
}
|
|
|
|
return i, nil
|
|
}
|
|
|
|
func checkAndRemoveUnderscoresIntegers(b []byte) ([]byte, error) {
|
|
start := 0
|
|
if b[start] == '+' || b[start] == '-' {
|
|
start++
|
|
}
|
|
|
|
if b[start] == '_' {
|
|
return nil, newDecodeError(b[start:start+1], "number cannot start with underscore")
|
|
}
|
|
|
|
if b[len(b)-1] == '_' {
|
|
return nil, newDecodeError(b[len(b)-1:], "number cannot end with underscore")
|
|
}
|
|
|
|
// fast path
|
|
i := 0
|
|
for ; i < len(b); i++ {
|
|
if b[i] == '_' {
|
|
break
|
|
}
|
|
}
|
|
if i == len(b) {
|
|
return b, nil
|
|
}
|
|
|
|
before := false
|
|
cleaned := make([]byte, i, len(b))
|
|
copy(cleaned, b)
|
|
|
|
for i++; i < len(b); i++ {
|
|
c := b[i]
|
|
if c == '_' {
|
|
if !before {
|
|
return nil, newDecodeError(b[i-1:i+1], "number must have at least one digit between underscores")
|
|
}
|
|
before = false
|
|
} else {
|
|
before = true
|
|
cleaned = append(cleaned, c)
|
|
}
|
|
}
|
|
|
|
return cleaned, nil
|
|
}
|
|
|
|
func checkAndRemoveUnderscoresFloats(b []byte) ([]byte, error) {
|
|
if b[0] == '_' {
|
|
return nil, newDecodeError(b[0:1], "number cannot start with underscore")
|
|
}
|
|
|
|
if b[len(b)-1] == '_' {
|
|
return nil, newDecodeError(b[len(b)-1:], "number cannot end with underscore")
|
|
}
|
|
|
|
// fast path
|
|
i := 0
|
|
for ; i < len(b); i++ {
|
|
if b[i] == '_' {
|
|
break
|
|
}
|
|
}
|
|
if i == len(b) {
|
|
return b, nil
|
|
}
|
|
|
|
before := false
|
|
cleaned := make([]byte, 0, len(b))
|
|
|
|
for i := 0; i < len(b); i++ {
|
|
c := b[i]
|
|
|
|
switch c {
|
|
case '_':
|
|
if !before {
|
|
return nil, newDecodeError(b[i-1:i+1], "number must have at least one digit between underscores")
|
|
}
|
|
if i < len(b)-1 && (b[i+1] == 'e' || b[i+1] == 'E') {
|
|
return nil, newDecodeError(b[i+1:i+2], "cannot have underscore before exponent")
|
|
}
|
|
before = false
|
|
case '+', '-':
|
|
// signed exponents
|
|
cleaned = append(cleaned, c)
|
|
before = false
|
|
case 'e', 'E':
|
|
if i < len(b)-1 && b[i+1] == '_' {
|
|
return nil, newDecodeError(b[i+1:i+2], "cannot have underscore after exponent")
|
|
}
|
|
cleaned = append(cleaned, c)
|
|
case '.':
|
|
if i < len(b)-1 && b[i+1] == '_' {
|
|
return nil, newDecodeError(b[i+1:i+2], "cannot have underscore after decimal point")
|
|
}
|
|
if i > 0 && b[i-1] == '_' {
|
|
return nil, newDecodeError(b[i-1:i], "cannot have underscore before decimal point")
|
|
}
|
|
cleaned = append(cleaned, c)
|
|
default:
|
|
before = true
|
|
cleaned = append(cleaned, c)
|
|
}
|
|
}
|
|
|
|
return cleaned, nil
|
|
}
|
|
|
|
// isValidDate checks if a provided date is a date that exists.
|
|
func isValidDate(year int, month int, day int) bool {
|
|
return month > 0 && month < 13 && day > 0 && day <= daysIn(month, year)
|
|
}
|
|
|
|
// daysBefore[m] counts the number of days in a non-leap year
|
|
// before month m begins. There is an entry for m=12, counting
|
|
// the number of days before January of next year (365).
|
|
var daysBefore = [...]int32{
|
|
0,
|
|
31,
|
|
31 + 28,
|
|
31 + 28 + 31,
|
|
31 + 28 + 31 + 30,
|
|
31 + 28 + 31 + 30 + 31,
|
|
31 + 28 + 31 + 30 + 31 + 30,
|
|
31 + 28 + 31 + 30 + 31 + 30 + 31,
|
|
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31,
|
|
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30,
|
|
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31,
|
|
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30,
|
|
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30 + 31,
|
|
}
|
|
|
|
func daysIn(m int, year int) int {
|
|
if m == 2 && isLeap(year) {
|
|
return 29
|
|
}
|
|
return int(daysBefore[m] - daysBefore[m-1])
|
|
}
|
|
|
|
func isLeap(year int) bool {
|
|
return year%4 == 0 && (year%100 != 0 || year%400 == 0)
|
|
}
|