Skip to content

Commit

Permalink
feat: Add timezone location support (#2705)
Browse files Browse the repository at this point in the history
Closes #2688.

The PR adds support for timezone locations by embedding an [IANA
timezone database](https://github.com/eggert/tz) that gets loaded into
memory when gno.land is started. The gno stdlib's time package has been
updated by porting over code from the go stdlib to support locations.

This feature allows users to create time instances with locations to
ensure that temporal adjustments result in accurate results by following
both historical rules and current rules.

<details><summary>Contributors' checklist...</summary>

- [x] Added new tests, or not needed, or not feasible
- [x] Provided an example (e.g. screenshot) to aid review or the PR is
self-explanatory
- [x] Updated the official documentation or not needed
- [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message
was included in the description
- [x] Added references to related issues and PRs
- [x] Provided any useful hints for running manual tests
- [x] Added new benchmarks to [generated
graphs](https://gnoland.github.io/benchmarks), if any. More info
[here](https://github.com/gnolang/gno/blob/master/.benchmarks/README.md).
</details>
  • Loading branch information
deelawn committed Sep 25, 2024
1 parent 9897b66 commit ca9eb4b
Show file tree
Hide file tree
Showing 7 changed files with 608 additions and 35 deletions.
34 changes: 34 additions & 0 deletions gnovm/stdlibs/generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

50 changes: 15 additions & 35 deletions gnovm/stdlibs/time/timezoneinfo.gno
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,8 @@ var errLocation = errors.New("time: invalid location name")

var zoneinfo *string

func loadFromEmbeddedTZData(name string) ([]byte, bool) // injected

// XXX var zoneinfoOnce sync.Once

// LoadLocation returns the Location with the given name.
Expand All @@ -640,41 +642,19 @@ var zoneinfo *string
// - $GOROOT/lib/time/zoneinfo.zip
// - the time/tzdata package, if it was imported
func LoadLocation(name string) (*Location, error) {
panic("XXX LoadLocation not yet implemented")
/*
if name == "" || name == "UTC" {
return UTC, nil
}
if name == "Local" {
return Local, nil
}
if containsDotDot(name) || name[0] == '/' || name[0] == '\\' {
// No valid IANA Time Zone name contains a single dot,
// much less dot dot. Likewise, none begin with a slash.
return nil, errLocation
}
zoneinfoOnce.Do(func() {
env, _ := syscall.Getenv("ZONEINFO")
zoneinfo = &env
})
var firstErr error
if *zoneinfo != "" {
if zoneData, err := loadTzinfoFromDirOrZip(*zoneinfo, name); err == nil {
if z, err := LoadLocationFromTZData(name, zoneData); err == nil {
return z, nil
}
firstErr = err
} else if err != syscall.ENOENT {
firstErr = err
}
}
if z, err := loadLocation(name, platformZoneSources); err == nil {
return z, nil
} else if firstErr == nil {
firstErr = err
}
return nil, firstErr
*/
if name == "" || name == "UTC" {
return UTC, nil
}
if name == "Local" {
return Local, nil
}
if containsDotDot(name) || name[0] == '/' || name[0] == '\\' {
// No valid IANA Time Zone name contains a single dot,
// much less dot dot. Likewise, none begin with a slash.
return nil, errLocation
}

return loadLocation(name)
}

// containsDotDot reports whether s contains "..".
Expand Down
74 changes: 74 additions & 0 deletions gnovm/stdlibs/time/tzdata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package time

// locationDefinitions are loaded during initialization using modified code from:
// https://cs.opensource.google/go/go/+/refs/tags/go1.23.0:src/time/tzdata/tzdata.go
var locationDefinitions = make(map[string][]byte)

func init() {
const (
zecheader = 0x06054b50
zcheader = 0x02014b50
ztailsize = 22

zheadersize = 30
zheader = 0x04034b50
)

z := zipdata

idx := len(z) - ztailsize
n := get2s(z[idx+10:])
idx = get4s(z[idx+16:])

for i := 0; i < n; i++ {
// See time.loadTzinfoFromZip for zip entry layout.
if get4s(z[idx:]) != zcheader {
break
}
meth := get2s(z[idx+10:])
size := get4s(z[idx+24:])
namelen := get2s(z[idx+28:])
xlen := get2s(z[idx+30:])
fclen := get2s(z[idx+32:])
off := get4s(z[idx+42:])
zname := z[idx+46 : idx+46+namelen]
idx += 46 + namelen + xlen + fclen

if meth != 0 {
panic("unsupported compression for " + zname + " in embedded tzdata")
}

// See time.loadTzinfoFromZip for zip per-file header layout.
nidx := off
if get4s(z[nidx:]) != zheader ||
get2s(z[nidx+8:]) != meth ||
get2s(z[nidx+26:]) != namelen {
panic("corrupt embedded tzdata")
}

nxlen := get2s(z[nidx+28:])
nidx += 30 + namelen + nxlen
locationDefinitions[zname] = []byte(z[nidx : nidx+size])
}
}

// get4s returns the little-endian 32-bit value at the start of s.
func get4s(s string) int {
if len(s) < 4 {
return 0
}
return int(s[0]) | int(s[1])<<8 | int(s[2])<<16 | int(s[3])<<24
}

// get2s returns the little-endian 16-bit value at the start of s.
func get2s(s string) int {
if len(s) < 2 {
return 0
}
return int(s[0]) | int(s[1])<<8
}

func X_loadFromEmbeddedTZData(name string) ([]byte, bool) {
definition, ok := locationDefinitions[name]
return definition, ok
}
Loading

0 comments on commit ca9eb4b

Please sign in to comment.