mirror of
				https://gitea.com/Lydanne/buildx.git
				synced 2025-11-04 18:13:42 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			446 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			446 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package stdlib
 | 
						|
 | 
						|
import (
 | 
						|
	"bufio"
 | 
						|
	"bytes"
 | 
						|
	"fmt"
 | 
						|
	"strings"
 | 
						|
	"time"
 | 
						|
 | 
						|
	"github.com/zclconf/go-cty/cty"
 | 
						|
	"github.com/zclconf/go-cty/cty/function"
 | 
						|
)
 | 
						|
 | 
						|
var FormatDateFunc = function.New(&function.Spec{
 | 
						|
	Description: `Formats a timestamp given in RFC 3339 syntax into another timestamp in some other machine-oriented time syntax, as described in the format string.`,
 | 
						|
	Params: []function.Parameter{
 | 
						|
		{
 | 
						|
			Name: "format",
 | 
						|
			Type: cty.String,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name: "time",
 | 
						|
			Type: cty.String,
 | 
						|
		},
 | 
						|
	},
 | 
						|
	Type:         function.StaticReturnType(cty.String),
 | 
						|
	RefineResult: refineNonNull,
 | 
						|
	Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
 | 
						|
		formatStr := args[0].AsString()
 | 
						|
		timeStr := args[1].AsString()
 | 
						|
		t, err := parseTimestamp(timeStr)
 | 
						|
		if err != nil {
 | 
						|
			return cty.DynamicVal, function.NewArgError(1, err)
 | 
						|
		}
 | 
						|
 | 
						|
		var buf bytes.Buffer
 | 
						|
		sc := bufio.NewScanner(strings.NewReader(formatStr))
 | 
						|
		sc.Split(splitDateFormat)
 | 
						|
		const esc = '\''
 | 
						|
		for sc.Scan() {
 | 
						|
			tok := sc.Bytes()
 | 
						|
 | 
						|
			// The leading byte signals the token type
 | 
						|
			switch {
 | 
						|
			case tok[0] == esc:
 | 
						|
				if tok[len(tok)-1] != esc || len(tok) == 1 {
 | 
						|
					return cty.DynamicVal, function.NewArgErrorf(0, "unterminated literal '")
 | 
						|
				}
 | 
						|
				if len(tok) == 2 {
 | 
						|
					// Must be a single escaped quote, ''
 | 
						|
					buf.WriteByte(esc)
 | 
						|
				} else {
 | 
						|
					// The content (until a closing esc) is printed out verbatim
 | 
						|
					// except that we must un-double any double-esc escapes in
 | 
						|
					// the middle of the string.
 | 
						|
					raw := tok[1 : len(tok)-1]
 | 
						|
					for i := 0; i < len(raw); i++ {
 | 
						|
						buf.WriteByte(raw[i])
 | 
						|
						if raw[i] == esc {
 | 
						|
							i++ // skip the escaped quote
 | 
						|
						}
 | 
						|
					}
 | 
						|
				}
 | 
						|
 | 
						|
			case startsDateFormatVerb(tok[0]):
 | 
						|
				switch tok[0] {
 | 
						|
				case 'Y':
 | 
						|
					y := t.Year()
 | 
						|
					switch len(tok) {
 | 
						|
					case 2:
 | 
						|
						fmt.Fprintf(&buf, "%02d", y%100)
 | 
						|
					case 4:
 | 
						|
						fmt.Fprintf(&buf, "%04d", y)
 | 
						|
					default:
 | 
						|
						return cty.DynamicVal, function.NewArgErrorf(0, "invalid date format verb %q: year must either be \"YY\" or \"YYYY\"", tok)
 | 
						|
					}
 | 
						|
				case 'M':
 | 
						|
					m := t.Month()
 | 
						|
					switch len(tok) {
 | 
						|
					case 1:
 | 
						|
						fmt.Fprintf(&buf, "%d", m)
 | 
						|
					case 2:
 | 
						|
						fmt.Fprintf(&buf, "%02d", m)
 | 
						|
					case 3:
 | 
						|
						buf.WriteString(m.String()[:3])
 | 
						|
					case 4:
 | 
						|
						buf.WriteString(m.String())
 | 
						|
					default:
 | 
						|
						return cty.DynamicVal, function.NewArgErrorf(0, "invalid date format verb %q: month must be \"M\", \"MM\", \"MMM\", or \"MMMM\"", tok)
 | 
						|
					}
 | 
						|
				case 'D':
 | 
						|
					d := t.Day()
 | 
						|
					switch len(tok) {
 | 
						|
					case 1:
 | 
						|
						fmt.Fprintf(&buf, "%d", d)
 | 
						|
					case 2:
 | 
						|
						fmt.Fprintf(&buf, "%02d", d)
 | 
						|
					default:
 | 
						|
						return cty.DynamicVal, function.NewArgErrorf(0, "invalid date format verb %q: day of month must either be \"D\" or \"DD\"", tok)
 | 
						|
					}
 | 
						|
				case 'E':
 | 
						|
					d := t.Weekday()
 | 
						|
					switch len(tok) {
 | 
						|
					case 3:
 | 
						|
						buf.WriteString(d.String()[:3])
 | 
						|
					case 4:
 | 
						|
						buf.WriteString(d.String())
 | 
						|
					default:
 | 
						|
						return cty.DynamicVal, function.NewArgErrorf(0, "invalid date format verb %q: day of week must either be \"EEE\" or \"EEEE\"", tok)
 | 
						|
					}
 | 
						|
				case 'h':
 | 
						|
					h := t.Hour()
 | 
						|
					switch len(tok) {
 | 
						|
					case 1:
 | 
						|
						fmt.Fprintf(&buf, "%d", h)
 | 
						|
					case 2:
 | 
						|
						fmt.Fprintf(&buf, "%02d", h)
 | 
						|
					default:
 | 
						|
						return cty.DynamicVal, function.NewArgErrorf(0, "invalid date format verb %q: 24-hour must either be \"h\" or \"hh\"", tok)
 | 
						|
					}
 | 
						|
				case 'H':
 | 
						|
					h := t.Hour() % 12
 | 
						|
					if h == 0 {
 | 
						|
						h = 12
 | 
						|
					}
 | 
						|
					switch len(tok) {
 | 
						|
					case 1:
 | 
						|
						fmt.Fprintf(&buf, "%d", h)
 | 
						|
					case 2:
 | 
						|
						fmt.Fprintf(&buf, "%02d", h)
 | 
						|
					default:
 | 
						|
						return cty.DynamicVal, function.NewArgErrorf(0, "invalid date format verb %q: 12-hour must either be \"H\" or \"HH\"", tok)
 | 
						|
					}
 | 
						|
				case 'A', 'a':
 | 
						|
					if len(tok) != 2 {
 | 
						|
						return cty.DynamicVal, function.NewArgErrorf(0, "invalid date format verb %q: must be \"%s%s\"", tok, tok[0:1], tok[0:1])
 | 
						|
					}
 | 
						|
					upper := tok[0] == 'A'
 | 
						|
					switch t.Hour() / 12 {
 | 
						|
					case 0:
 | 
						|
						if upper {
 | 
						|
							buf.WriteString("AM")
 | 
						|
						} else {
 | 
						|
							buf.WriteString("am")
 | 
						|
						}
 | 
						|
					case 1:
 | 
						|
						if upper {
 | 
						|
							buf.WriteString("PM")
 | 
						|
						} else {
 | 
						|
							buf.WriteString("pm")
 | 
						|
						}
 | 
						|
					}
 | 
						|
				case 'm':
 | 
						|
					m := t.Minute()
 | 
						|
					switch len(tok) {
 | 
						|
					case 1:
 | 
						|
						fmt.Fprintf(&buf, "%d", m)
 | 
						|
					case 2:
 | 
						|
						fmt.Fprintf(&buf, "%02d", m)
 | 
						|
					default:
 | 
						|
						return cty.DynamicVal, function.NewArgErrorf(0, "invalid date format verb %q: minute must either be \"m\" or \"mm\"", tok)
 | 
						|
					}
 | 
						|
				case 's':
 | 
						|
					s := t.Second()
 | 
						|
					switch len(tok) {
 | 
						|
					case 1:
 | 
						|
						fmt.Fprintf(&buf, "%d", s)
 | 
						|
					case 2:
 | 
						|
						fmt.Fprintf(&buf, "%02d", s)
 | 
						|
					default:
 | 
						|
						return cty.DynamicVal, function.NewArgErrorf(0, "invalid date format verb %q: second must either be \"s\" or \"ss\"", tok)
 | 
						|
					}
 | 
						|
				case 'Z':
 | 
						|
					// We'll just lean on Go's own formatter for this one, since
 | 
						|
					// the necessary information is unexported.
 | 
						|
					switch len(tok) {
 | 
						|
					case 1:
 | 
						|
						buf.WriteString(t.Format("Z07:00"))
 | 
						|
					case 3:
 | 
						|
						str := t.Format("-0700")
 | 
						|
						switch str {
 | 
						|
						case "+0000":
 | 
						|
							buf.WriteString("UTC")
 | 
						|
						default:
 | 
						|
							buf.WriteString(str)
 | 
						|
						}
 | 
						|
					case 4:
 | 
						|
						buf.WriteString(t.Format("-0700"))
 | 
						|
					case 5:
 | 
						|
						buf.WriteString(t.Format("-07:00"))
 | 
						|
					default:
 | 
						|
						return cty.DynamicVal, function.NewArgErrorf(0, "invalid date format verb %q: timezone must be Z, ZZZZ, or ZZZZZ", tok)
 | 
						|
					}
 | 
						|
				default:
 | 
						|
					return cty.DynamicVal, function.NewArgErrorf(0, "invalid date format verb %q", tok)
 | 
						|
				}
 | 
						|
 | 
						|
			default:
 | 
						|
				// Any other starting character indicates a literal sequence
 | 
						|
				buf.Write(tok)
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		return cty.StringVal(buf.String()), nil
 | 
						|
	},
 | 
						|
})
 | 
						|
 | 
						|
// TimeAddFunc is a function that adds a duration to a timestamp, returning a new timestamp.
 | 
						|
var TimeAddFunc = function.New(&function.Spec{
 | 
						|
	Description: `Adds the duration represented by the given duration string to the given RFC 3339 timestamp string, returning another RFC 3339 timestamp.`,
 | 
						|
	Params: []function.Parameter{
 | 
						|
		{
 | 
						|
			Name: "timestamp",
 | 
						|
			Type: cty.String,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name: "duration",
 | 
						|
			Type: cty.String,
 | 
						|
		},
 | 
						|
	},
 | 
						|
	Type: function.StaticReturnType(cty.String),
 | 
						|
	Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
 | 
						|
		ts, err := parseTimestamp(args[0].AsString())
 | 
						|
		if err != nil {
 | 
						|
			return cty.UnknownVal(cty.String), err
 | 
						|
		}
 | 
						|
		duration, err := time.ParseDuration(args[1].AsString())
 | 
						|
		if err != nil {
 | 
						|
			return cty.UnknownVal(cty.String), err
 | 
						|
		}
 | 
						|
 | 
						|
		return cty.StringVal(ts.Add(duration).Format(time.RFC3339)), nil
 | 
						|
	},
 | 
						|
})
 | 
						|
 | 
						|
// FormatDate reformats a timestamp given in RFC3339 syntax into another time
 | 
						|
// syntax defined by a given format string.
 | 
						|
//
 | 
						|
// The format string uses letter mnemonics to represent portions of the
 | 
						|
// timestamp, with repetition signifying length variants of each portion.
 | 
						|
// Single quote characters ' can be used to quote sequences of literal letters
 | 
						|
// that should not be interpreted as formatting mnemonics.
 | 
						|
//
 | 
						|
// The full set of supported mnemonic sequences is listed below:
 | 
						|
//
 | 
						|
//     YY       Year modulo 100 zero-padded to two digits, like "06".
 | 
						|
//     YYYY     Four (or more) digit year, like "2006".
 | 
						|
//     M        Month number, like "1" for January.
 | 
						|
//     MM       Month number zero-padded to two digits, like "01".
 | 
						|
//     MMM      English month name abbreviated to three letters, like "Jan".
 | 
						|
//     MMMM     English month name unabbreviated, like "January".
 | 
						|
//     D        Day of month number, like "2".
 | 
						|
//     DD       Day of month number zero-padded to two digits, like "02".
 | 
						|
//     EEE      English day of week name abbreviated to three letters, like "Mon".
 | 
						|
//     EEEE     English day of week name unabbreviated, like "Monday".
 | 
						|
//     h        24-hour number, like "2".
 | 
						|
//     hh       24-hour number zero-padded to two digits, like "02".
 | 
						|
//     H        12-hour number, like "2".
 | 
						|
//     HH       12-hour number zero-padded to two digits, like "02".
 | 
						|
//     AA       Hour AM/PM marker in uppercase, like "AM".
 | 
						|
//     aa       Hour AM/PM marker in lowercase, like "am".
 | 
						|
//     m        Minute within hour, like "5".
 | 
						|
//     mm       Minute within hour zero-padded to two digits, like "05".
 | 
						|
//     s        Second within minute, like "9".
 | 
						|
//     ss       Second within minute zero-padded to two digits, like "09".
 | 
						|
//     ZZZZ     Timezone offset with just sign and digit, like "-0800".
 | 
						|
//     ZZZZZ    Timezone offset with colon separating hours and minutes, like "-08:00".
 | 
						|
//     Z        Like ZZZZZ but with a special case "Z" for UTC.
 | 
						|
//     ZZZ      Like ZZZZ but with a special case "UTC" for UTC.
 | 
						|
//
 | 
						|
// The format syntax is optimized mainly for generating machine-oriented
 | 
						|
// timestamps rather than human-oriented timestamps; the English language
 | 
						|
// portions of the output reflect the use of English names in a number of
 | 
						|
// machine-readable date formatting standards. For presentation to humans,
 | 
						|
// a locale-aware time formatter (not included in this package) is a better
 | 
						|
// choice.
 | 
						|
//
 | 
						|
// The format syntax is not compatible with that of any other language, but
 | 
						|
// is optimized so that patterns for common standard date formats can be
 | 
						|
// recognized quickly even by a reader unfamiliar with the format syntax.
 | 
						|
func FormatDate(format cty.Value, timestamp cty.Value) (cty.Value, error) {
 | 
						|
	return FormatDateFunc.Call([]cty.Value{format, timestamp})
 | 
						|
}
 | 
						|
 | 
						|
// splitDataFormat is a bufio.SplitFunc used to tokenize a date format.
 | 
						|
func splitDateFormat(data []byte, atEOF bool) (advance int, token []byte, err error) {
 | 
						|
	if len(data) == 0 {
 | 
						|
		return 0, nil, nil
 | 
						|
	}
 | 
						|
 | 
						|
	const esc = '\''
 | 
						|
 | 
						|
	switch {
 | 
						|
 | 
						|
	case data[0] == esc:
 | 
						|
		// If we have another quote immediately after then this is a single
 | 
						|
		// escaped escape.
 | 
						|
		if len(data) > 1 && data[1] == esc {
 | 
						|
			return 2, data[:2], nil
 | 
						|
		}
 | 
						|
 | 
						|
		// Beginning of quoted sequence, so we will seek forward until we find
 | 
						|
		// the closing quote, ignoring escaped quotes along the way.
 | 
						|
		for i := 1; i < len(data); i++ {
 | 
						|
			if data[i] == esc {
 | 
						|
				if (i + 1) == len(data) {
 | 
						|
					if atEOF {
 | 
						|
						// We have a closing quote and are at the end of our input
 | 
						|
						return len(data), data, nil
 | 
						|
					} else {
 | 
						|
						// We need at least one more byte to decide if this is an
 | 
						|
						// escape or a terminator.
 | 
						|
						return 0, nil, nil
 | 
						|
					}
 | 
						|
				}
 | 
						|
				if data[i+1] == esc {
 | 
						|
					i++ // doubled-up quotes are an escape sequence
 | 
						|
					continue
 | 
						|
				}
 | 
						|
				// We've found the closing quote
 | 
						|
				return i + 1, data[:i+1], nil
 | 
						|
			}
 | 
						|
		}
 | 
						|
		// If we fall out here then we need more bytes to find the end,
 | 
						|
		// unless we're already at the end with an unclosed quote.
 | 
						|
		if atEOF {
 | 
						|
			return len(data), data, nil
 | 
						|
		}
 | 
						|
		return 0, nil, nil
 | 
						|
 | 
						|
	case startsDateFormatVerb(data[0]):
 | 
						|
		rep := data[0]
 | 
						|
		for i := 1; i < len(data); i++ {
 | 
						|
			if data[i] != rep {
 | 
						|
				return i, data[:i], nil
 | 
						|
			}
 | 
						|
		}
 | 
						|
		if atEOF {
 | 
						|
			return len(data), data, nil
 | 
						|
		}
 | 
						|
		// We need more data to decide if we've found the end
 | 
						|
		return 0, nil, nil
 | 
						|
 | 
						|
	default:
 | 
						|
		for i := 1; i < len(data); i++ {
 | 
						|
			if data[i] == esc || startsDateFormatVerb(data[i]) {
 | 
						|
				return i, data[:i], nil
 | 
						|
			}
 | 
						|
		}
 | 
						|
		// We might not actually be at the end of a literal sequence,
 | 
						|
		// but that doesn't matter since we'll concat them back together
 | 
						|
		// anyway.
 | 
						|
		return len(data), data, nil
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func startsDateFormatVerb(b byte) bool {
 | 
						|
	return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z')
 | 
						|
}
 | 
						|
 | 
						|
func parseTimestamp(ts string) (time.Time, error) {
 | 
						|
	t, err := parseStrictRFC3339(ts)
 | 
						|
	if err != nil {
 | 
						|
		switch err := err.(type) {
 | 
						|
		case *time.ParseError:
 | 
						|
			// If err is s time.ParseError then its string representation is not
 | 
						|
			// appropriate since it relies on details of Go's strange date format
 | 
						|
			// representation, which a caller of our functions is not expected
 | 
						|
			// to be familiar with.
 | 
						|
			//
 | 
						|
			// Therefore we do some light transformation to get a more suitable
 | 
						|
			// error that should make more sense to our callers. These are
 | 
						|
			// still not awesome error messages, but at least they refer to
 | 
						|
			// the timestamp portions by name rather than by Go's example
 | 
						|
			// values.
 | 
						|
			if err.LayoutElem == "" && err.ValueElem == "" && err.Message != "" {
 | 
						|
				// For some reason err.Message is populated with a ": " prefix
 | 
						|
				// by the time package.
 | 
						|
				return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp%s", err.Message)
 | 
						|
			}
 | 
						|
			var what string
 | 
						|
			switch err.LayoutElem {
 | 
						|
			case "2006":
 | 
						|
				what = "year"
 | 
						|
			case "01":
 | 
						|
				what = "month"
 | 
						|
			case "02":
 | 
						|
				what = "day of month"
 | 
						|
			case "15":
 | 
						|
				what = "hour"
 | 
						|
			case "04":
 | 
						|
				what = "minute"
 | 
						|
			case "05":
 | 
						|
				what = "second"
 | 
						|
			case "Z07:00":
 | 
						|
				what = "UTC offset"
 | 
						|
			case "T":
 | 
						|
				return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: missing required time introducer 'T'")
 | 
						|
			case ":", "-":
 | 
						|
				if err.ValueElem == "" {
 | 
						|
					return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: end of string where %q is expected", err.LayoutElem)
 | 
						|
				} else {
 | 
						|
					return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: found %q where %q is expected", err.ValueElem, err.LayoutElem)
 | 
						|
				}
 | 
						|
			default:
 | 
						|
				// Should never get here, because RFC3339 includes only the
 | 
						|
				// above portions.
 | 
						|
				what = "timestamp segment"
 | 
						|
			}
 | 
						|
			if err.ValueElem == "" {
 | 
						|
				return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: end of string before %s", what)
 | 
						|
			} else {
 | 
						|
				switch {
 | 
						|
				case what == "hour" && strings.Contains(err.ValueElem, ":"):
 | 
						|
					return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: hour must be between 0 and 23 inclusive")
 | 
						|
				case what == "hour" && len(err.ValueElem) != 2:
 | 
						|
					return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: hour must have exactly two digits")
 | 
						|
				case what == "minute" && len(err.ValueElem) != 2:
 | 
						|
					return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: minute must have exactly two digits")
 | 
						|
				default:
 | 
						|
					return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: cannot use %q as %s", err.ValueElem, what)
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
		return time.Time{}, err
 | 
						|
	}
 | 
						|
	return t, nil
 | 
						|
}
 | 
						|
 | 
						|
// TimeAdd adds a duration to a timestamp, returning a new timestamp.
 | 
						|
//
 | 
						|
// In the HCL language, timestamps are conventionally represented as
 | 
						|
// strings using RFC 3339 "Date and Time format" syntax. Timeadd requires
 | 
						|
// the timestamp argument to be a string conforming to this syntax.
 | 
						|
//
 | 
						|
// `duration` is a string representation of a time difference, consisting of
 | 
						|
// sequences of number and unit pairs, like `"1.5h"` or `1h30m`. The accepted
 | 
						|
// units are `ns`, `us` (or `µs`), `"ms"`, `"s"`, `"m"`, and `"h"`. The first
 | 
						|
// number may be negative to indicate a negative duration, like `"-2h5m"`.
 | 
						|
//
 | 
						|
// The result is a string, also in RFC 3339 format, representing the result
 | 
						|
// of adding the given direction to the given timestamp.
 | 
						|
func TimeAdd(timestamp cty.Value, duration cty.Value) (cty.Value, error) {
 | 
						|
	return TimeAddFunc.Call([]cty.Value{timestamp, duration})
 | 
						|
}
 |