These two land together on purpose. Fixing the encoder alone changes nothing for the blobs already in the zone, because emit skips whatever is already present. #86 — klauspost/compress omits the zstd Frame_Content_Size field for inputs under 256 bytes, which the format permits. Reference libzstd never does, so the NWN client — which sizes its output buffer from ZSTD_getFrameContentSize and has therefore never met a frame without one — rejected roughly 6% of our blobs outright. Any single one stops a sync dead, so no client could complete a sync of the live manifest. No encoder option changes this, so emit re-headers the affected frames into the shape libzstd itself emits: Single_Segment_flag set, Window_Descriptor dropped, and the freed byte spent on a one-byte Frame_Content_Size. Same length in, same length out. Verified against the zstd CLI end to end: a real emitted 230-byte blob now reports its decompressed size where it previously reported none. emitter_version goes to 2, so assemble refuses to merge an index written by the encoder that omitted the field. #85 — a published blob was verified by exactly one thing, a player's client, at download time. `nwsync verify <manifest-sha1>` now reads a manifest and its blobs back through the public pull zone with no credential, decompresses and hashes every one, and reports missing / malformed framing / size mismatch / hash mismatch per blob. `--sample N` makes a routine check cheap against a manifest that is ~69,000 blobs and 15 GB. `emit --verify` applies the same check where emit would otherwise trust presence, and replaces a stored blob that is not what its name claims — which is what makes the #86 blobs repairable. Both new checks assert the frame property, not just a round trip. Round-tripping cannot see this class of fault: both the zstd CLI and Go's decoder stream such a frame happily, being more capable decoders than the client's, which is how the defect reached production and survived an audit. Refs #54, #55, #59, #75, sow-platform#79. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
532 lines
16 KiB
Go
532 lines
16 KiB
Go
package nwsync
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha1"
|
|
"crypto/sha256"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.westgate.pw/ShadowsOverWestgate/sow-tools/internal/erf"
|
|
)
|
|
|
|
func restype(t *testing.T, extension string) uint16 {
|
|
t.Helper()
|
|
value, ok := erf.ResourceTypeForExtension(extension)
|
|
if !ok {
|
|
t.Fatalf("unknown restype %q", extension)
|
|
}
|
|
return value
|
|
}
|
|
|
|
// writeHak builds a HAK fixture from resref.ext => body pairs.
|
|
func writeHak(t *testing.T, path string, contents map[string][]byte) {
|
|
t.Helper()
|
|
resources := make([]erf.Resource, 0, len(contents))
|
|
for name, body := range contents {
|
|
stem, extension, _ := strings.Cut(name, ".")
|
|
resources = append(resources, erf.Resource{
|
|
Name: stem,
|
|
Type: restype(t, extension),
|
|
Data: body,
|
|
Size: int64(len(body)),
|
|
})
|
|
}
|
|
var out bytes.Buffer
|
|
if err := erf.Write(&out, erf.New("HAK", resources)); err != nil {
|
|
t.Fatalf("write hak: %v", err)
|
|
}
|
|
if err := os.WriteFile(path, out.Bytes(), 0o644); err != nil {
|
|
t.Fatalf("write hak file: %v", err)
|
|
}
|
|
}
|
|
|
|
// artifactKey is the depot key a file would be published under: the sha256 of
|
|
// its bytes, hash-tree depth 2, keeping the extension.
|
|
func artifactKey(t *testing.T, path string) string {
|
|
t.Helper()
|
|
body, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
sum := sha256.Sum256(body)
|
|
digest := hex.EncodeToString(sum[:])
|
|
return "artifacts/haks/sha256/" + digest[0:2] + "/" + digest[2:4] + "/" + digest + filepath.Ext(path)
|
|
}
|
|
|
|
// emitLocal emits one artifact into a local tree, the conformance path.
|
|
func emitLocal(t *testing.T, path, out string) (EmitResult, error) {
|
|
t.Helper()
|
|
return Emit(EmitOptions{
|
|
ArtifactKey: artifactKey(t, path),
|
|
ArtifactPath: path,
|
|
As: filepath.Base(path),
|
|
OutDir: out,
|
|
})
|
|
}
|
|
|
|
func TestBlobFramingRoundTrips(t *testing.T) {
|
|
data := []byte("the quick brown fox jumps over the lazy dog, repeatedly and at length")
|
|
blob := compressBlob(data)
|
|
|
|
header := make([]uint32, 6)
|
|
if err := binary.Read(bytes.NewReader(blob[:blobHeaderBytes]), binary.LittleEndian, header); err != nil {
|
|
t.Fatalf("read header: %v", err)
|
|
}
|
|
want := []uint32{blobMagic, 3, 2, uint32(len(data)), 1, 0}
|
|
for i := range want {
|
|
if header[i] != want[i] {
|
|
t.Errorf("header field %d = %d, want %d", i, header[i], want[i])
|
|
}
|
|
}
|
|
if string(blob[:4]) != "NSYC" {
|
|
t.Errorf("magic bytes = %q, want NSYC", blob[:4])
|
|
}
|
|
|
|
got, err := decompressBlob(blob)
|
|
if err != nil {
|
|
t.Fatalf("decompress: %v", err)
|
|
}
|
|
if !bytes.Equal(got, data) {
|
|
t.Errorf("round trip mismatch: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestManifestBytesMatchUpstreamLayout(t *testing.T) {
|
|
shared := sha1.Sum([]byte("shared"))
|
|
other := sha1.Sum([]byte("other"))
|
|
// Deliberately out of order, and with two resrefs sharing one sha1: the
|
|
// second one must become a mapping, not a second entry.
|
|
entries := []Entry{
|
|
{SHA1: other, Size: 5, ResRef: "zzz", ResType: 1},
|
|
{SHA1: shared, Size: 6, ResRef: "bbb", ResType: 2},
|
|
{SHA1: shared, Size: 6, ResRef: "aaa", ResType: 3},
|
|
}
|
|
data, err := writeManifest(entries)
|
|
if err != nil {
|
|
t.Fatalf("write manifest: %v", err)
|
|
}
|
|
|
|
if string(data[:4]) != "NSYM" {
|
|
t.Fatalf("magic = %q", data[:4])
|
|
}
|
|
var version, entryCount, mappingCount uint32
|
|
reader := bytes.NewReader(data[4:16])
|
|
for _, field := range []*uint32{&version, &entryCount, &mappingCount} {
|
|
_ = binary.Read(reader, binary.LittleEndian, field)
|
|
}
|
|
if version != 3 || entryCount != 2 || mappingCount != 1 {
|
|
t.Fatalf("header = version %d, %d entries, %d mappings; want 3/2/1", version, entryCount, mappingCount)
|
|
}
|
|
wantSize := 16 + int(entryCount)*(20+4+16+2) + int(mappingCount)*(4+16+2)
|
|
if len(data) != wantSize {
|
|
t.Fatalf("manifest is %d bytes, want %d", len(data), wantSize)
|
|
}
|
|
|
|
// Sorted by sha1 hex then resref, so the shared hash's "aaa" is the entry
|
|
// and "bbb" is demoted to a mapping.
|
|
round, err := readManifest(data)
|
|
if err != nil {
|
|
t.Fatalf("read manifest: %v", err)
|
|
}
|
|
if len(round) != 3 {
|
|
t.Fatalf("round trip returned %d entries, want 3", len(round))
|
|
}
|
|
byResRef := map[string]Entry{}
|
|
for _, entry := range round {
|
|
byResRef[entry.ResRef] = entry
|
|
}
|
|
for _, entry := range entries {
|
|
got, ok := byResRef[entry.ResRef]
|
|
if !ok {
|
|
t.Fatalf("resref %q lost in round trip", entry.ResRef)
|
|
}
|
|
if got != entry {
|
|
t.Errorf("resref %q = %+v, want %+v", entry.ResRef, got, entry)
|
|
}
|
|
}
|
|
if round[0].ResRef != "aaa" && round[1].ResRef != "aaa" {
|
|
t.Errorf("entries are not sorted by sha1 then resref: %+v", round)
|
|
}
|
|
}
|
|
|
|
func TestEmitWritesBlobsAndManifest(t *testing.T) {
|
|
dir := t.TempDir()
|
|
hak := filepath.Join(dir, "sow_test_01.hak")
|
|
body := []byte("texture bytes")
|
|
writeHak(t, hak, map[string][]byte{
|
|
"bloodstain1.tga": body,
|
|
"copy1.txi": body, // same content, different resref: one blob
|
|
"script1.nss": []byte("void main() {}"),
|
|
"debug1.ndb": []byte("debug"),
|
|
"comment1.gic": []byte("comment"),
|
|
})
|
|
|
|
out := filepath.Join(dir, "out")
|
|
result, err := emitLocal(t, hak, out)
|
|
if err != nil {
|
|
t.Fatalf("emit: %v", err)
|
|
}
|
|
if result.Entries != 2 {
|
|
t.Errorf("emitted %d entries, want 2 (nss/ndb/gic are always skipped)", result.Entries)
|
|
}
|
|
if result.BlobsWritten != 1 {
|
|
t.Errorf("wrote %d blobs, want 1 (identical content shares a blob)", result.BlobsWritten)
|
|
}
|
|
|
|
// The blob is named by the sha1 of the uncompressed bytes, under a depth-2
|
|
// hash tree, and decompresses back to exactly those bytes.
|
|
sum := sha1.Sum(body)
|
|
name := hex.EncodeToString(sum[:])
|
|
path := filepath.Join(out, "data", "sha1", name[0:2], name[2:4], name)
|
|
blob, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("blob missing at %s: %v", path, err)
|
|
}
|
|
got, err := decompressBlob(blob)
|
|
if err != nil {
|
|
t.Fatalf("decompress blob: %v", err)
|
|
}
|
|
if !bytes.Equal(got, body) {
|
|
t.Errorf("blob decompressed to %q, want %q", got, body)
|
|
}
|
|
|
|
entries := readEmitted(t, result.ManifestPath)
|
|
for _, entry := range entries {
|
|
if entry.ResType == restype(t, "nss") || entry.ResType == restype(t, "ndb") || entry.ResType == restype(t, "gic") {
|
|
t.Errorf("skipped restype leaked into the manifest: %+v", entry)
|
|
}
|
|
}
|
|
|
|
var sidecar Sidecar
|
|
body2, err := os.ReadFile(result.ManifestPath + ".json")
|
|
if err != nil {
|
|
t.Fatalf("sidecar missing: %v", err)
|
|
}
|
|
if err := json.Unmarshal(body2, &sidecar); err != nil {
|
|
t.Fatalf("sidecar json: %v", err)
|
|
}
|
|
if sidecar.TotalFiles != 2 || sidecar.HashTreeDepth != 2 || sidecar.Version != 3 {
|
|
t.Errorf("sidecar = %+v", sidecar)
|
|
}
|
|
if sidecar.IncludesModuleContents {
|
|
t.Error("sidecar claims module contents; a published manifest never has them")
|
|
}
|
|
if strings.Contains(string(body2), "group_id") {
|
|
t.Error("per-artifact sidecar should omit group_id (0 means absent)")
|
|
}
|
|
if _, err := os.Stat(filepath.Join(out, "latest")); err == nil {
|
|
t.Error("a latest file was written; there must never be one")
|
|
}
|
|
}
|
|
|
|
func readEmitted(t *testing.T, indexPath string) []Entry {
|
|
t.Helper()
|
|
data, err := os.ReadFile(indexPath)
|
|
if err != nil {
|
|
t.Fatalf("read emitted manifest: %v", err)
|
|
}
|
|
entries, err := readManifest(data)
|
|
if err != nil {
|
|
t.Fatalf("parse emitted manifest: %v", err)
|
|
}
|
|
return entries
|
|
}
|
|
|
|
func TestEmitFailsClosedOnOversizeResource(t *testing.T) {
|
|
dir := t.TempDir()
|
|
hak := filepath.Join(dir, "big.hak")
|
|
writeHak(t, hak, map[string][]byte{"huge1.tga": make([]byte, fileSizeLimit+1)})
|
|
|
|
if _, err := emitLocal(t, hak, filepath.Join(dir, "out")); err == nil {
|
|
t.Fatal("emit accepted a resource over the 15 MB limit")
|
|
}
|
|
}
|
|
|
|
func TestEmitLooseFile(t *testing.T) {
|
|
dir := t.TempDir()
|
|
tlk := filepath.Join(dir, "sow_tlk.tlk")
|
|
if err := os.WriteFile(tlk, []byte("TLK V3.0 payload"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out := filepath.Join(dir, "out")
|
|
result, err := emitLocal(t, tlk, out)
|
|
if err != nil {
|
|
t.Fatalf("emit tlk: %v", err)
|
|
}
|
|
entries := readEmitted(t, result.ManifestPath)
|
|
if len(entries) != 1 || entries[0].ResRef != "sow_tlk" || entries[0].ResType != restype(t, "tlk") {
|
|
t.Fatalf("tlk emitted as %+v", entries)
|
|
}
|
|
if result.BlobsWritten != 1 {
|
|
t.Errorf("wrote %d blobs, want 1", result.BlobsWritten)
|
|
}
|
|
}
|
|
|
|
// emitFixture emits two haks that share a resref, so the merge rule is
|
|
// observable: "top" holds the winning body, "assets" the shadowed one.
|
|
func emitFixture(t *testing.T) (out string, keys map[string]string, topBody, assetBody []byte) {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
topBody = []byte("2da from sow_top")
|
|
assetBody = []byte("2da from the asset hak")
|
|
writeHak(t, filepath.Join(dir, "sow_top.hak"), map[string][]byte{"appearance.2da": topBody})
|
|
writeHak(t, filepath.Join(dir, "sow_core_01.hak"), map[string][]byte{
|
|
"appearance.2da": assetBody,
|
|
"bloodstain1.tga": []byte("blood"),
|
|
})
|
|
out = filepath.Join(dir, "out")
|
|
keys = map[string]string{}
|
|
for _, name := range []string{"sow_top", "sow_core_01"} {
|
|
path := filepath.Join(dir, name+".hak")
|
|
keys[name] = artifactKey(t, path)
|
|
if _, err := emitLocal(t, path, out); err != nil {
|
|
t.Fatalf("emit %s: %v", name, err)
|
|
}
|
|
}
|
|
return out, keys, topBody, assetBody
|
|
}
|
|
|
|
func TestAssembleShadowsByOrder(t *testing.T) {
|
|
entriesDir, keys, topBody, assetBody := emitFixture(t)
|
|
|
|
result, err := Assemble(AssembleOptions{
|
|
ArtifactKeys: []string{keys["sow_top"], keys["sow_core_01"]},
|
|
OutDir: entriesDir,
|
|
GroupID: 2,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("assemble: %v", err)
|
|
}
|
|
if result.Entries != 2 {
|
|
t.Fatalf("merged %d entries, want 2 (appearance.2da is shadowed, not duplicated)", result.Entries)
|
|
}
|
|
|
|
data, err := os.ReadFile(result.ManifestPath)
|
|
if err != nil {
|
|
t.Fatalf("read merged manifest: %v", err)
|
|
}
|
|
merged := sha1.Sum(data)
|
|
if hex.EncodeToString(merged[:]) != result.SHA1 || filepath.Base(result.ManifestPath) != result.SHA1 {
|
|
t.Errorf("manifest is not named by its own sha1: %s", result.ManifestPath)
|
|
}
|
|
entries, err := readManifest(data)
|
|
if err != nil {
|
|
t.Fatalf("parse merged manifest: %v", err)
|
|
}
|
|
for _, entry := range entries {
|
|
if entry.ResRef != "appearance" {
|
|
continue
|
|
}
|
|
if entry.SHA1 != sha1.Sum(topBody) {
|
|
t.Errorf("appearance.2da resolved to the wrong hak; want the earliest in --order")
|
|
}
|
|
if entry.SHA1 == sha1.Sum(assetBody) {
|
|
t.Error("appearance.2da resolved to the shadowed hak")
|
|
}
|
|
}
|
|
|
|
sidecar := Sidecar{}
|
|
body, err := os.ReadFile(result.ManifestPath + ".json")
|
|
if err != nil {
|
|
t.Fatalf("merged sidecar missing: %v", err)
|
|
}
|
|
if err := json.Unmarshal(body, &sidecar); err != nil {
|
|
t.Fatalf("merged sidecar json: %v", err)
|
|
}
|
|
if sidecar.GroupID != 2 {
|
|
t.Errorf("group_id = %d, want 2 (testing)", sidecar.GroupID)
|
|
}
|
|
if sidecar.SHA1 != result.SHA1 {
|
|
t.Errorf("sidecar sha1 = %s, want %s", sidecar.SHA1, result.SHA1)
|
|
}
|
|
if sidecar.TotalFiles != 2 {
|
|
t.Errorf("total_files = %d, want 2", sidecar.TotalFiles)
|
|
}
|
|
}
|
|
|
|
func TestAssembleReversedOrderPicksTheOtherHak(t *testing.T) {
|
|
entriesDir, keys, topBody, assetBody := emitFixture(t)
|
|
|
|
result, err := Assemble(AssembleOptions{
|
|
ArtifactKeys: []string{keys["sow_core_01"], keys["sow_top"]},
|
|
OutDir: entriesDir,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("assemble: %v", err)
|
|
}
|
|
data, _ := os.ReadFile(result.ManifestPath)
|
|
entries, err := readManifest(data)
|
|
if err != nil {
|
|
t.Fatalf("parse merged manifest: %v", err)
|
|
}
|
|
for _, entry := range entries {
|
|
if entry.ResRef == "appearance" && entry.SHA1 != sha1.Sum(assetBody) {
|
|
t.Errorf("appearance.2da did not follow --order; still resolves to %x", entry.SHA1)
|
|
}
|
|
}
|
|
_ = topBody
|
|
}
|
|
|
|
func TestAssembleRefusesMismatchedEmitterVersions(t *testing.T) {
|
|
entriesDir, keys, _, _ := emitFixture(t)
|
|
|
|
index, err := resolveIndexKey(keys["sow_core_01"], entriesDir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
path := filepath.Join(entriesDir, index+".json")
|
|
var sidecar Sidecar
|
|
body, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := json.Unmarshal(body, &sidecar); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
sidecar.EmitterVersion = "0"
|
|
patched, err := marshalSidecar(sidecar)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(path, patched, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err = Assemble(AssembleOptions{
|
|
ArtifactKeys: []string{keys["sow_top"], keys["sow_core_01"]},
|
|
OutDir: entriesDir,
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "emitter version mismatch") {
|
|
t.Fatalf("assemble merged across emitter versions: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestEmitHonoursSourceDateEpoch(t *testing.T) {
|
|
t.Setenv("SOURCE_DATE_EPOCH", "1700000000")
|
|
dir := t.TempDir()
|
|
hak := filepath.Join(dir, "pinned.hak")
|
|
writeHak(t, hak, map[string][]byte{"one1.tga": []byte("body")})
|
|
result, err := emitLocal(t, hak, filepath.Join(dir, "out"))
|
|
if err != nil {
|
|
t.Fatalf("emit: %v", err)
|
|
}
|
|
body, err := os.ReadFile(result.ManifestPath + ".json")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var sidecar Sidecar
|
|
if err := json.Unmarshal(body, &sidecar); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if sidecar.Created != 1700000000 {
|
|
t.Errorf("created = %d, want the pinned SOURCE_DATE_EPOCH", sidecar.Created)
|
|
}
|
|
}
|
|
|
|
func TestEmitRejectsAModule(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "sow.mod")
|
|
var out bytes.Buffer
|
|
if err := erf.Write(&out, erf.New("MOD", []erf.Resource{
|
|
{Name: "module", Type: restype(t, "ifo"), Data: []byte("ifo"), Size: 3},
|
|
})); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(path, out.Bytes(), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := emitLocal(t, path, filepath.Join(dir, "out")); err == nil {
|
|
t.Fatal("emit accepted a .mod; a manifest never carries module contents")
|
|
}
|
|
}
|
|
|
|
func TestAssembleFailsClosedOnMissingIndex(t *testing.T) {
|
|
entriesDir, keys, _, _ := emitFixture(t)
|
|
missing := "artifacts/haks/sha256/00/11/" + strings.Repeat("0", 64) + ".hak"
|
|
_, err := Assemble(AssembleOptions{
|
|
ArtifactKeys: []string{keys["sow_top"], missing},
|
|
OutDir: entriesDir,
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), missing) {
|
|
t.Fatalf("assemble did not fail closed and name the missing artifact: %v", err)
|
|
}
|
|
}
|
|
|
|
// Callers capture a script's stdout as a value: `dir="$(pack-haks.sh)"`. A
|
|
// summary line on stdout gets glued onto that value, so both summaries belong
|
|
// on stderr.
|
|
func TestRunKeepsSummariesOffStdout(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "sow_top.hak")
|
|
writeHak(t, path, map[string][]byte{"appearance.2da": []byte("2da from sow_top")})
|
|
key := artifactKey(t, path)
|
|
out := filepath.Join(dir, "out")
|
|
|
|
for _, args := range [][]string{
|
|
{"emit", "--out", out, "--as", "sow_top.hak", key, path},
|
|
{"assemble", "--out", out, key},
|
|
} {
|
|
var stdout, stderr bytes.Buffer
|
|
if code := Run(args, &stdout, &stderr); code != exitOK {
|
|
t.Fatalf("Run(%v) exit=%d: %s", args, code, stderr.String())
|
|
}
|
|
if stdout.Len() != 0 {
|
|
t.Errorf("Run(%v) wrote to stdout: %q", args, stdout.String())
|
|
}
|
|
if stderr.Len() == 0 {
|
|
t.Errorf("Run(%v) reported no summary on stderr", args)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRunUsageErrors(t *testing.T) {
|
|
cases := [][]string{
|
|
nil,
|
|
{"nope"},
|
|
{"emit"},
|
|
{"emit", "artifact-key.hak"},
|
|
{"emit", "a", "b", "c"},
|
|
{"assemble", "--out", "y"},
|
|
}
|
|
for _, args := range cases {
|
|
var out, errw bytes.Buffer
|
|
if code := Run(args, &out, &errw); code != exitUsage {
|
|
t.Errorf("Run(%v) exit=%d, want %d", args, code, exitUsage)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestEveryBlobDeclaresItsFrameContentSize guards the fault that stopped every
|
|
// client sync (#86): klauspost/compress omits Frame_Content_Size for inputs
|
|
// under 256 bytes, and the game client cannot decode a frame without it. This
|
|
// asserts a frame property, not a round trip — the zstd CLI and Go's decoder
|
|
// both stream such a frame happily, so round-tripping cannot see the defect.
|
|
func TestEveryBlobDeclaresItsFrameContentSize(t *testing.T) {
|
|
// 230 and 175 are real sizes from the manifest that failed to sync; 255/256
|
|
// straddle the encoder's threshold.
|
|
for _, size := range []int{1, 32, 175, 230, 255, 256, 257, 1024, 5000} {
|
|
payload := make([]byte, size)
|
|
for i := range payload {
|
|
payload[i] = byte('a' + i%26)
|
|
}
|
|
blob := compressBlob(payload)
|
|
if !frameDeclaresContentSize(blob[blobHeaderBytes:]) {
|
|
t.Errorf("blob of %d bytes declares no frame content size (descriptor %#x)",
|
|
size, blob[blobHeaderBytes+4])
|
|
}
|
|
got, err := decompressBlob(blob)
|
|
if err != nil {
|
|
t.Fatalf("decompress %d-byte blob: %v", size, err)
|
|
}
|
|
if !bytes.Equal(got, payload) {
|
|
t.Errorf("%d-byte blob did not round trip", size)
|
|
}
|
|
}
|
|
}
|