clone

an offline map explorer where each world, from a game map to a city's open data, ships as one .atlas file

main
atlas / internal / generate / tiles / stamp.go
stamp.go 5.4K
1package tiles23import (4 "crypto/sha256"5 "embed"6 "encoding/hex"7 "fmt"8 "io/fs"9 "sort"10 "sync"11)1213// Every pyramid used to be derived afresh on every run: adding one volume14// re-decoded and re-reduced the other seventeen, half a minute of work to arrive15// at the bytes that were already there. A pyramid is derived from tiles stored16// under their own content hash, so what went into one can be written down and17// compared, and a pyramid whose capture has not moved is carried over from the18// last run instead.19//20// The deriving code counts as an input. Changing how a level is reduced, or21// where the content bounds land, has to invalidate every pyramid, and a stamp22// that only watched the archive would quietly keep serving the old derivation.23// That is also why a stamp is not portable between implementations: two derivers24// that write identical bytes still stamp differently, because they are different25// tools, and the stamp's promise is "nothing that made this has moved" rather26// than "these bytes are these bytes". docs/stamps.md carries what that27// costs and what is proven instead.28//29// The deriving code is every file that decides what a pyramid's bytes are --30// which now includes the renderer, because a city's deepest level is drawn31// rather than copied and a change to the drawing has to invalidate that city's32// pyramid the same way a change to the reduction invalidates every other one,33// and the warp, because the same donor through a different transformation is a34// different picture.35//36//go:embed plan.go stamp.go derive.go draw.go warp.go basemap37var toolSource embed.FS3839// ToolStamp is the hash of the deriving code itself, computed once. It is40// exported so a test can say what it is holding a plan against: a fixture41// records the stamp the sources that drew it had, so proving the *plan* half42// of a recorded stamp means substituting that tool hash for this one.43var ToolStamp = sync.OnceValue(func() string {44 var names []string45 err := fs.WalkDir(toolSource, ".", func(path string, entry fs.DirEntry, err error) error {46 if err != nil {47 return err48 }49 if !entry.IsDir() {50 names = append(names, path)51 }52 return nil53 })54 if err != nil {55 // The files are embedded at build time, so this cannot fail in a built56 // binary. Falling back to a random-looking constant would defeat the57 // point of the stamp, and rebuilding everything is the safe answer.58 return "unstamped"59 }60 sort.Strings(names)61 sum := sha256.New()62 for _, name := range names {63 data, err := toolSource.ReadFile(name)64 if err != nil {65 return "unstamped"66 }67 fmt.Fprintf(sum, "%s\x00%d\x00", name, len(data))68 sum.Write(data)69 }70 return hex.EncodeToString(sum.Sum(nil))71})7273// PlanStamp names everything a pyramid is derived from: the tool that derives74// it, the shape the plan settled on, and the content hash of every captured tile75// that goes into it. Two runs that would write the same bytes stamp the same.76//77// The field order and the NUL framing are the contract, spelled out in78// docs/generate.md ยง4.2, and they are frozen: a stamp is folded into a bundle's79// stamp, so re-spelling one restamps every bundle in every library.80func PlanStamp(plan Plan) string { return StampWith(plan, ToolStamp()) }8182// StampWith is PlanStamp with the tool's identity supplied rather than measured.83// It exists for one caller: the gate that proves the plan half of a recorded84// stamp by handing it the tool hash that stamp was taken with and asking85// whether the same stamp comes back. Nothing in the lane calls it, and nothing86// should -- a pyramid is stamped by the tool that derived it or not at all.87func StampWith(plan Plan, tool string) string {88 sum := sha256.New()89 fmt.Fprintf(sum, "tool\x00%s\x00", tool)90 fmt.Fprintf(sum, "layer\x00%s\x00%s\x00", plan.TileSet, plan.Name)91 fmt.Fprintf(sum, "zooms\x00%d\x00%d\x00", plan.MaxFullZoom, plan.MaxSourceZoom)92 fmt.Fprintf(sum, "format\x00%s\x00%t\x00", plan.Format, plan.Interpolate)93 if plan.Bounds != nil {94 fmt.Fprintf(sum, "bounds\x00%d\x00%d\x00%d\x00%d\x00",95 plan.Bounds.X, plan.Bounds.Y, plan.Bounds.Width, plan.Bounds.Height)96 }97 // A warped pyramid is derived from its alignment as much as from its tiles:98 // the same donor resampled through a different transformation is a different99 // picture.100 if plan.Warp != nil {101 fmt.Fprintf(sum, "warp\x00%s\x00%d\x00%d\x00%d\x00",102 plan.Warp.Base.TileSet, plan.Warp.TargetZoom,103 plan.Warp.Base.Frame.BaseZoom, plan.Warp.Base.Frame.BaseTile)104 a := plan.Warp.Affine105 fmt.Fprintf(sum, "%.9f\x00%.9f\x00%.9f\x00%.9f\x00%.9f\x00%.9f\x00",106 a.AX, a.BX, a.CX, a.AY, a.BY, a.CY)107 }108109 zooms := make([]int, 0, len(plan.Levels))110 for zoom := range plan.Levels {111 zooms = append(zooms, zoom)112 }113 sort.Ints(zooms)114 for _, zoom := range zooms {115 tiles := append([]Tile(nil), plan.Levels[zoom]...)116 // Sorted by column and then row, which is not the order a level is117 // walked in: the listing order on disk is deliberately not an input, so118 // two archives holding the same tiles stamp alike however they list them.119 sort.Slice(tiles, func(i, j int) bool {120 if tiles[i].Ref.X != tiles[j].Ref.X {121 return tiles[i].Ref.X < tiles[j].Ref.X122 }123 return tiles[i].Ref.Y < tiles[j].Ref.Y124 })125 fmt.Fprintf(sum, "level\x00%d\x00%d\x00", zoom, len(tiles))126 for _, tile := range tiles {127 fmt.Fprintf(sum, "%d\x00%d\x00%s\x00%s\x00",128 tile.Ref.X, tile.Ref.Y, tile.Ref.ContentHash, tile.Format)129 }130 }131 return hex.EncodeToString(sum.Sum(nil))132}