| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "os" |
| 5 | "regexp" |
| 6 | "strings" |
| 7 | ) |
| 8 | |
| 9 | // tzName is the shape of an IANA zone name the console will accept (spec |
| 10 | // §6.2 step 1). Anything else is not sent: the console then falls back to |
| 11 | // the request's IP, then the home. |
| 12 | var tzName = regexp.MustCompile(`^[A-Za-z0-9_+\-]+(/[A-Za-z0-9_+\-]+){0,2}$`) |
| 13 | |
| 14 | var notAZone = map[string]bool{"localtime": true, "posixrules": true, "Local": true} |
| 15 | |
| 16 | // localTimezone finds this machine's IANA zone name, portably (no build |
| 17 | // tags; the 20-cross gate builds it for darwin, windows and linux-arm64): |
| 18 | // $TZ, else the /etc/localtime symlink target, else /etc/timezone, else "". |
| 19 | // Windows has none of these, so only a valid $TZ is ever sent there. |
| 20 | func localTimezone(getenv func(string) string, readlink func(string) (string, error), readFile func(string) ([]byte, error)) string { |
| 21 | if tz := clean(strings.TrimPrefix(strings.TrimSpace(getenv("TZ")), ":")); tz != "" { |
| 22 | return tz |
| 23 | } |
| 24 | if target, err := readlink("/etc/localtime"); err == nil { |
| 25 | if tz := clean(target); tz != "" { |
| 26 | return tz |
| 27 | } |
| 28 | } |
| 29 | if b, err := readFile("/etc/timezone"); err == nil { |
| 30 | line, _, _ := strings.Cut(string(b), "\n") |
| 31 | return clean(line) |
| 32 | } |
| 33 | return "" |
| 34 | } |
| 35 | |
| 36 | func clean(s string) string { |
| 37 | s = strings.TrimSpace(s) |
| 38 | if _, after, ok := strings.Cut(s, "zoneinfo/"); ok { |
| 39 | s = after |
| 40 | } |
| 41 | if s == "" || strings.HasPrefix(s, "/") || notAZone[s] || len(s) > 64 || !tzName.MatchString(s) { |
| 42 | return "" |
| 43 | } |
| 44 | return s |
| 45 | } |
| 46 | |
| 47 | func detectTimezone() string { return localTimezone(os.Getenv, os.Readlink, os.ReadFile) } |
| 48 | |