mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 14:52:30 -05:00 
			
		
		
		
	
		
			
				
	
	
		
			550 lines
		
	
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			550 lines
		
	
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package toml
 | |
| 
 | |
| import (
 | |
| 	"fmt"
 | |
| 	"math"
 | |
| 	"strconv"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/pelletier/go-toml/v2/unstable"
 | |
| )
 | |
| 
 | |
| 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, unstable.NewParserError(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{}, unstable.NewParserError(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, unstable.NewParserError(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{}, unstable.NewParserError(b, "invalid date-time timezone")
 | |
| 		}
 | |
| 		var direction int
 | |
| 		switch b[0] {
 | |
| 		case '-':
 | |
| 			direction = -1
 | |
| 		case '+':
 | |
| 			direction = +1
 | |
| 		default:
 | |
| 			return time.Time{}, unstable.NewParserError(b[:1], "invalid timezone offset character")
 | |
| 		}
 | |
| 
 | |
| 		if b[3] != ':' {
 | |
| 			return time.Time{}, unstable.NewParserError(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{}, unstable.NewParserError(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{}, unstable.NewParserError(b[:1], "invalid timezone offset minutes")
 | |
| 		}
 | |
| 
 | |
| 		seconds := direction * (hours*3600 + minutes*60)
 | |
| 		if seconds == 0 {
 | |
| 			zone = time.UTC
 | |
| 		} else {
 | |
| 			zone = time.FixedZone("", seconds)
 | |
| 		}
 | |
| 		b = b[dateTimeByteLen:]
 | |
| 	}
 | |
| 
 | |
| 	if len(b) > 0 {
 | |
| 		return time.Time{}, unstable.NewParserError(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, unstable.NewParserError(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, unstable.NewParserError(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, unstable.NewParserError(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, unstable.NewParserError(b[0:2], "hour cannot be greater 23")
 | |
| 	}
 | |
| 	if b[2] != ':' {
 | |
| 		return t, nil, unstable.NewParserError(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, unstable.NewParserError(b[3:5], "minutes cannot be greater 59")
 | |
| 	}
 | |
| 	if b[5] != ':' {
 | |
| 		return t, nil, unstable.NewParserError(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, unstable.NewParserError(b[6:8], "seconds cannot be greater 60")
 | |
| 	}
 | |
| 
 | |
| 	b = b[8:]
 | |
| 
 | |
| 	if len(b) >= 1 && b[0] == '.' {
 | |
| 		frac := 0
 | |
| 		precision := 0
 | |
| 		digits := 0
 | |
| 
 | |
| 		for i, c := range b[1:] {
 | |
| 			if !isDigit(c) {
 | |
| 				if i == 0 {
 | |
| 					return t, nil, unstable.NewParserError(b[0:1], "need at least one digit after fraction point")
 | |
| 				}
 | |
| 				break
 | |
| 			}
 | |
| 			digits++
 | |
| 
 | |
| 			const maxFracPrecision = 9
 | |
| 			if i >= maxFracPrecision {
 | |
| 				// go-toml allows decoding fractional seconds
 | |
| 				// beyond the supported precision of 9
 | |
| 				// digits. It truncates the fractional component
 | |
| 				// to the supported precision and ignores the
 | |
| 				// remaining digits.
 | |
| 				//
 | |
| 				// https://github.com/pelletier/go-toml/discussions/707
 | |
| 				continue
 | |
| 			}
 | |
| 
 | |
| 			frac *= 10
 | |
| 			frac += int(c - '0')
 | |
| 			precision++
 | |
| 		}
 | |
| 
 | |
| 		if precision == 0 {
 | |
| 			return t, nil, unstable.NewParserError(b[:1], "nanoseconds need at least one digit")
 | |
| 		}
 | |
| 
 | |
| 		t.Nanosecond = frac * nspow[precision]
 | |
| 		t.Precision = precision
 | |
| 
 | |
| 		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, unstable.NewParserError(b, "float cannot start with a dot")
 | |
| 	}
 | |
| 
 | |
| 	if cleaned[len(cleaned)-1] == '.' {
 | |
| 		return 0, unstable.NewParserError(b, "float cannot end with a dot")
 | |
| 	}
 | |
| 
 | |
| 	dotAlreadySeen := false
 | |
| 	for i, c := range cleaned {
 | |
| 		if c == '.' {
 | |
| 			if dotAlreadySeen {
 | |
| 				return 0, unstable.NewParserError(b[i:i+1], "float can have at most one decimal point")
 | |
| 			}
 | |
| 			if !isDigit(cleaned[i-1]) {
 | |
| 				return 0, unstable.NewParserError(b[i-1:i+1], "float decimal point must be preceded by a digit")
 | |
| 			}
 | |
| 			if !isDigit(cleaned[i+1]) {
 | |
| 				return 0, unstable.NewParserError(b[i:i+2], "float decimal point must be followed by a digit")
 | |
| 			}
 | |
| 			dotAlreadySeen = true
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	start := 0
 | |
| 	if cleaned[0] == '+' || cleaned[0] == '-' {
 | |
| 		start = 1
 | |
| 	}
 | |
| 	if cleaned[start] == '0' && len(cleaned) > start+1 && isDigit(cleaned[start+1]) {
 | |
| 		return 0, unstable.NewParserError(b, "float integer part cannot have leading zeroes")
 | |
| 	}
 | |
| 
 | |
| 	f, err := strconv.ParseFloat(string(cleaned), 64)
 | |
| 	if err != nil {
 | |
| 		return 0, unstable.NewParserError(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, unstable.NewParserError(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, unstable.NewParserError(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, unstable.NewParserError(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, unstable.NewParserError(b, "leading zero not allowed on decimal number")
 | |
| 	}
 | |
| 
 | |
| 	i, err := strconv.ParseInt(string(cleaned), 10, 64)
 | |
| 	if err != nil {
 | |
| 		return 0, unstable.NewParserError(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 len(b) == start {
 | |
| 		return b, nil
 | |
| 	}
 | |
| 
 | |
| 	if b[start] == '_' {
 | |
| 		return nil, unstable.NewParserError(b[start:start+1], "number cannot start with underscore")
 | |
| 	}
 | |
| 
 | |
| 	if b[len(b)-1] == '_' {
 | |
| 		return nil, unstable.NewParserError(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, unstable.NewParserError(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, unstable.NewParserError(b[0:1], "number cannot start with underscore")
 | |
| 	}
 | |
| 
 | |
| 	if b[len(b)-1] == '_' {
 | |
| 		return nil, unstable.NewParserError(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, unstable.NewParserError(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, unstable.NewParserError(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, unstable.NewParserError(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, unstable.NewParserError(b[i+1:i+2], "cannot have underscore after decimal point")
 | |
| 			}
 | |
| 			if i > 0 && b[i-1] == '_' {
 | |
| 				return nil, unstable.NewParserError(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)
 | |
| }
 | |
| 
 | |
| func isDigit(r byte) bool {
 | |
| 	return r >= '0' && r <= '9'
 | |
| }
 |