fix(nwsync): pin emitter version, ship the shim, harden manifest reads
ci / ci (pull_request) Successful in 3m28s
ci / ci (pull_request) Successful in 3m28s
Review follow-ups on the same work: - emitter_version is its own sidecar field, not the build revision. Keying the merge check on created_with invalidated every published index on every unrelated crucible commit, forcing a re-emit of the whole corpus. - flake.nix builds cmd/crucible-nwsync, so Nix consumers get the binary the docs promise. - readManifest uses io.ReadFull: bytes.Reader.Read can return a short count with no error, so a truncated manifest parsed into zero-padded garbage. - emit refuses a .mod outright and names why, rather than failing on an unknown extension. - SOURCE_DATE_EPOCH pins the sidecar timestamp; the manifest was already deterministic. - Entry.identity replaces the resref/restype key struct duplicated in emit and assemble. Refs #53.
This commit is contained in:
+35
-12
@@ -8,6 +8,7 @@ import (
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -23,8 +24,17 @@ const fileSizeLimit = 15 * 1024 * 1024
|
||||
// skippedTypes are never published, matching upstream's GobalResTypeSkipList.
|
||||
var skippedTypes = resTypes("nss", "ndb", "gic")
|
||||
|
||||
// emitterVersion identifies the blob/manifest byte format this package
|
||||
// produces. assemble refuses to merge indexes that disagree on it, because two
|
||||
// producers of blobs mean a skewed emitter can otherwise write blobs the merged
|
||||
// manifest quietly disagrees with. Bump it only when emitted bytes change — it
|
||||
// is deliberately not the build revision, which would invalidate every
|
||||
// published index on every unrelated commit.
|
||||
const emitterVersion = "1"
|
||||
|
||||
// serverTypes are loaded only server-side; a manifest holding nothing else
|
||||
// has no client contents. Mirrors upstream's GlobalResTypeServerList.
|
||||
// has no client contents. Mirrors upstream's GlobalResTypeServerList, whose
|
||||
// trailing 0 is RESTYPE_INVALID.
|
||||
var serverTypes = append(resTypes(
|
||||
"are", "dlg", "fac", "gic", "git", "ifo", "itp", "jrl", "ncs", "ndb",
|
||||
"nss", "ptm", "utc", "utd", "ute", "uti", "utm", "utp", "uts", "utt", "utw",
|
||||
@@ -88,12 +98,17 @@ func readArtifact(path string) ([]erf.Resource, error) {
|
||||
}
|
||||
if len(data) >= 3 {
|
||||
switch string(data[:3]) {
|
||||
case "ERF", "HAK", "MOD":
|
||||
case "ERF", "HAK":
|
||||
archive, err := erf.Read(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", path, err)
|
||||
}
|
||||
return archive.Resources, nil
|
||||
case "MOD":
|
||||
// A persistent world never publishes module contents, so the .mod
|
||||
// contributes no bytes to a manifest — it only says which haks and
|
||||
// which TLK the manifest covers.
|
||||
return nil, fmt.Errorf("%s: a module is never emitted; a manifest is haks plus the TLK", path)
|
||||
}
|
||||
}
|
||||
base := filepath.Base(path)
|
||||
@@ -112,12 +127,8 @@ func readArtifact(path string) ([]erf.Resource, error) {
|
||||
func emitResources(resources []erf.Resource, outDir string) ([]Entry, int, int64, error) {
|
||||
// A resref appearing twice inside one artifact resolves to the last one,
|
||||
// the way resman lets the last container added win.
|
||||
type key struct {
|
||||
resref string
|
||||
restype uint16
|
||||
}
|
||||
order := make([]key, 0, len(resources))
|
||||
latest := make(map[key]erf.Resource, len(resources))
|
||||
order := make([]Identity, 0, len(resources))
|
||||
latest := make(map[Identity]erf.Resource, len(resources))
|
||||
var tooBig []string
|
||||
for _, resource := range resources {
|
||||
if _, ok := erf.ExtensionForResourceType(resource.Type); !ok {
|
||||
@@ -130,7 +141,7 @@ func emitResources(resources []erf.Resource, outDir string) ([]Entry, int, int64
|
||||
tooBig = append(tooBig, fmt.Sprintf("%s: %d bytes > %d", resource.Name, len(resource.Data), fileSizeLimit))
|
||||
continue
|
||||
}
|
||||
identity := key{resref: strings.ToLower(resource.Name), restype: resource.Type}
|
||||
identity := Identity{ResRef: strings.ToLower(resource.Name), ResType: resource.Type}
|
||||
if _, seen := latest[identity]; !seen {
|
||||
order = append(order, identity)
|
||||
}
|
||||
@@ -150,8 +161,8 @@ func emitResources(resources []erf.Resource, outDir string) ([]Entry, int, int64
|
||||
entries = append(entries, Entry{
|
||||
SHA1: sum,
|
||||
Size: uint32(len(resource.Data)),
|
||||
ResRef: identity.resref,
|
||||
ResType: identity.restype,
|
||||
ResRef: identity.ResRef,
|
||||
ResType: identity.ResType,
|
||||
})
|
||||
written, err := writeBlob(outDir, sum, resource.Data)
|
||||
if err != nil {
|
||||
@@ -165,6 +176,17 @@ func emitResources(resources []erf.Resource, outDir string) ([]Entry, int, int64
|
||||
return entries, blobs, onDiskBytes, nil
|
||||
}
|
||||
|
||||
// created is the sidecar timestamp. SOURCE_DATE_EPOCH pins it so a build can
|
||||
// be reproduced byte for byte; the manifest itself is deterministic already.
|
||||
func created() int64 {
|
||||
if raw := os.Getenv("SOURCE_DATE_EPOCH"); raw != "" {
|
||||
if seconds, err := strconv.ParseInt(raw, 10, 64); err == nil {
|
||||
return seconds
|
||||
}
|
||||
}
|
||||
return time.Now().Unix()
|
||||
}
|
||||
|
||||
// writeBlob writes one NWCompressedBuffer blob and returns its size on disk,
|
||||
// or 0 if the blob already existed. Blob names are content hashes, so an
|
||||
// existing name is existing content.
|
||||
@@ -210,8 +232,9 @@ func writeManifestPair(manifestPath string, data []byte, entries []Entry, onDisk
|
||||
sidecar.TotalFiles = len(entries)
|
||||
sidecar.TotalBytes = totalBytes
|
||||
sidecar.OnDiskBytes = onDiskBytes
|
||||
sidecar.Created = time.Now().Unix()
|
||||
sidecar.Created = created()
|
||||
sidecar.CreatedWith = buildinfo.String()
|
||||
sidecar.EmitterVersion = emitterVersion
|
||||
|
||||
body, err := marshalSidecar(sidecar)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user