Skip to content
go-libfossil Artifact [da842d76fd]
Login

Artifact [da842d76fd]

Artifact da842d76fdd18fbac15e89507b02fcb819ddbcd6e06ebfca0d2cba326aa001cc:


package conformance

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"path/filepath"
	"slices"
	"strings"
	"testing"
	"time"

	"github.com/danmestas/go-libfossil/internal/blob"
	"github.com/danmestas/go-libfossil/internal/cone"
	"github.com/danmestas/go-libfossil/internal/content"
	"github.com/danmestas/go-libfossil/internal/deck"
	libfossil "github.com/danmestas/go-libfossil/internal/fsltype"
	"github.com/danmestas/go-libfossil/internal/hash"
	"github.com/danmestas/go-libfossil/internal/manifest"
	"github.com/danmestas/go-libfossil/internal/repo"
	"github.com/danmestas/go-libfossil/internal/sync"
	"github.com/danmestas/go-libfossil/simio"
)

func scRecoveryCases() []scCase {
	return []scCase{
		{Case{ID: "sparsecone.4.2.recovery-max", Req: "SC-4.2-3"},
			func(t *testing.T) error { return scCanonicalRecovery(t, "max", 1) }},
		{Case{ID: "sparsecone.4.2.recovery-final-merge", Req: "SC-4.2-3"},
			func(t *testing.T) error { return scCanonicalRecovery(t, "final", 3) }},
		{Case{ID: "sparsecone.4.2.recovery-changed-k", Req: "SC-4.2-3"},
			func(t *testing.T) error { return scCanonicalRecovery(t, "replace", 2) }},
		{Case{ID: "sparsecone.4.2.recovery-unknown-k", Req: "SC-4.2-3"},
			func(t *testing.T) error { return scCanonicalRecovery(t, "replace", 1) }},
	}
}

// Direct execution never treats a deviation marker as a passing assertion.
func TestSparseRecovery(t *testing.T) {
	for _, c := range scRecoveryCases() {
		t.Run(c.ID, func(t *testing.T) {
			if err := c.run(t); err != nil {
				t.Fatal(err)
			}
		})
	}
}

// These are Appendix A.6's literal names, not a search using the writer's
// boundary function: the 18 names with zbits zero force MAX propagation.
var scRecoveryMAXNames = []string{
	"f0000", "f0001", "f0003", "f0004", "f0005", "f0006", "f0008", "f0010", "f0014",
	"f0015", "f0016", "f0017", "f0018", "f0019", "f0021", "f0022", "f0023", "f0024",
}

var scRecoveryTime = time.Date(2026, 10, 20, 0, 0, 0, 0, time.UTC)

type scRecoveryFixture struct {
	peer               *scSyncPeer
	dense, sparse      *repo.Repo
	parentUUID         string
	parentRID          libfossil.FslID
	oldNames, newNames []string
	cone               *cone.Cone
}

// Both clones use the same coverage policy, but only one actually loses S/F
// bytes. Their resulting check-in and every canonical child must be identical.
func scCanonicalRecovery(t *testing.T, mode string, k int) error {
	fixture, err := scRecoveryOrigin(t, mode)
	if err != nil {
		return err
	}
	if err := fixture.configure(t, mode, k); err != nil {
		return err
	}
	if err := fixture.elide(mode); err != nil {
		return err
	}
	if err := fixture.recover(); err != nil {
		return err
	}
	files, cards := fixture.files(mode)
	var output [][]byte
	for _, r := range []*repo.Repo{fixture.dense, fixture.sparse} {
		rid, ok := blob.Exists(r.DB(), fixture.parentUUID)
		if !ok {
			return expectf("parent identity not held")
		}
		newRID, _, err := manifest.Checkin(r, manifest.CheckinOpts{
			Files: files, Parent: rid, User: "u", Comment: "A.6 canonical rewrite",
			Time: scRecoveryTime.Add(time.Hour),
		})
		if err != nil {
			return err
		}
		data, err := content.Expand(r.DB(), newRID)
		if err != nil {
			return err
		}
		output = append(output, data)
	}
	if !bytes.Equal(output[0], output[1]) {
		return expectf("sparse reconstruction differs byte-for-byte from dense canonical commit")
	}
	if err := fixture.checkCanonical(mode, k, cards, output[1]); err != nil {
		return err
	}
	for _, name := range fixture.oldNames {
		if !fixture.cone.Covers(name) {
			if _, held := content.AvailableByUUID(fixture.sparse.DB(), hash.SHA3([]byte(name))); held {
				return expectf("structural recovery fetched outside F payload %s", name)
			}
		}
	}
	return nil
}

func scRecoveryOrigin(t *testing.T, mode string) (*scRecoveryFixture, error) {
	fixture := &scRecoveryFixture{
		oldNames: append([]string{scRecoveryMAXNames[0]}, scRecoveryMAXNames[2:]...),
		newNames: slices.Clone(scRecoveryMAXNames),
	}
	oldK := 1
	if mode == "replace" {
		fixture.newNames = slices.Clone(fixture.oldNames)
	}
	if mode == "final" {
		fixture.oldNames = []string{"g0000", "g0001", "g0005", "g0006"}
		fixture.newNames = []string{"g0000", "g0001", "g0005"}
		oldK = 3
	}
	r, err := repo.Create(filepath.Join(t.TempDir(), "origin.fossil"), "u", simio.CryptoRand{}, "")
	if err != nil {
		return nil, err
	}
	t.Cleanup(func() { r.Close() })
	if err := r.SetHashPolicy("sha3"); err != nil {
		return nil, err
	}
	for key, value := range map[string]string{
		manifest.SettingEnableSubmanifests: "on", manifest.SettingSubmanifestK: fmt.Sprint(oldK),
	} {
		if err := r.SetConfig(key, value); err != nil {
			return nil, err
		}
	}
	files := make([]manifest.File, 0, len(fixture.oldNames))
	for _, name := range fixture.oldNames {
		files = append(files, manifest.File{Name: name, Content: []byte(name)})
	}
	_, fixture.parentUUID, err = manifest.Checkin(r, manifest.CheckinOpts{
		Files: files, User: "u", Comment: "A.6 parent", Time: scRecoveryTime,
	})
	if err != nil {
		return nil, err
	}
	fixture.peer = &scSyncPeer{r: r}
	fixture.dense, _ = scSyncClone(t, fixture.peer, sync.CloneOpts{PullPhantoms: true})
	fixture.sparse, _ = scSyncClone(t, fixture.peer, sync.CloneOpts{PullPhantoms: true})
	return fixture, nil
}

func (f *scRecoveryFixture) configure(t *testing.T, mode string, k int) error {
	var ok bool
	f.parentRID, ok = blob.Exists(f.sparse.DB(), f.parentUUID)
	if !ok {
		return expectf("parent identity not cloned")
	}
	parent, err := manifest.GetManifest(f.sparse, f.parentRID)
	if err != nil {
		return err
	}
	if len(parent.S) < 2 {
		return expectf("A.6 parent has no sibling sub-manifest")
	}
	kept := 0
	if mode == "final" {
		kept = 1
	}
	keptRID, ok := content.AvailableByUUID(f.sparse.DB(), parent.S[kept].UUID)
	if !ok {
		return expectf("fixture leaf not held")
	}
	keptBytes, err := content.Expand(f.sparse.DB(), keptRID)
	if err != nil {
		return err
	}
	keptLeaf, err := deck.Parse(keptBytes)
	if err != nil {
		return err
	}
	var include []string
	for _, file := range keptLeaf.F {
		include = append(include, file.Name)
	}
	if mode == "max" {
		include = append(include, "f0001")
	}
	f.cone = scCone(t, include, nil)
	for _, r := range []*repo.Repo{f.dense, f.sparse} {
		if err := r.SetHashPolicy("sha3"); err != nil {
			return err
		}
		if err := content.SetStoreCone(r.DB(), f.cone); err != nil {
			return err
		}
		if err := r.SetConfig(manifest.SettingEnableSubmanifests, "on"); err != nil {
			return err
		}
		if err := r.SetConfig(manifest.SettingSubmanifestK, fmt.Sprint(k)); err != nil {
			return err
		}
	}
	return nil
}

func (f *scRecoveryFixture) elide(mode string) error {
	parent, err := manifest.GetManifest(f.sparse, f.parentRID)
	if err != nil {
		return err
	}
	kept := 0
	if mode == "final" {
		kept = 1
	}
	var elided []string
	for i, s := range parent.S {
		if i != kept {
			elided = append(elided, s.UUID)
		}
	}
	for _, name := range f.oldNames {
		if !f.cone.Covers(name) {
			elided = append(elided, hash.SHA3([]byte(name)))
		}
	}
	for _, uuid := range elided {
		_, err := f.sparse.DB().Exec("UPDATE blob SET size=-1,content=NULL WHERE uuid=?", uuid)
		if err != nil {
			return err
		}
		_, err = f.sparse.DB().Exec(
			"INSERT OR IGNORE INTO phantom(rid) SELECT rid FROM blob WHERE uuid=?", uuid)
		if err != nil {
			return err
		}
		if _, held := content.AvailableByUUID(f.sparse.DB(), uuid); held {
			return expectf("fixture did not actually remove %s", uuid)
		}
	}
	return nil
}

func (f *scRecoveryFixture) recover() error {
	var refused *manifest.RequiredContentError
	err := manifest.RequireParentContent(context.Background(), f.sparse.DB(),
		[]libfossil.FslID{f.parentRID})
	if !errors.As(err, &refused) {
		return expectf("absent S sibling was not explicitly refused: %v", err)
	}
	if refused.Elided == 0 || !strings.Contains(refused.Error(), "no configured peer") {
		return expectf("incorrect elision diagnostic: %v", refused)
	}
	if strings.Contains(refused.Error(), "missing") || strings.Contains(refused.Error(), "corrupt") {
		return expectf("elision described as damage: %v", refused)
	}
	_, err = sync.FetchRequiredArtifacts(context.Background(), f.sparse, f.peer,
		refused.Roots, sync.SyncOpts{})
	if err != nil {
		return err
	}
	got, err := content.StoreCone(f.sparse.DB())
	if err != nil || got == nil || !got.Equal(f.cone) {
		return expectf("recovery changed policy: %v, %v", got, err)
	}
	return nil
}

func (f *scRecoveryFixture) files(mode string) ([]manifest.File, []deck.FileCard) {
	files := make([]manifest.File, 0, len(f.newNames))
	cards := make([]deck.FileCard, 0, len(f.newNames))
	for _, name := range f.newNames {
		data := []byte(name)
		if mode == "replace" && name == "f0000" {
			data = []byte("replacement")
		}
		file := manifest.File{Name: name, UUID: hash.SHA3(data)}
		if f.cone.Covers(name) {
			file.UUID, file.Content = "", data
		}
		files = append(files, file)
		cards = append(cards, deck.FileCard{Name: name, UUID: hash.SHA3(data)})
	}
	return files, cards
}

func (f *scRecoveryFixture) checkCanonical(
	mode string, k int, cards []deck.FileCard, output []byte,
) error {
	canonical, err := manifest.BuildTree(cards, k, func(data []byte) (string, error) {
		return hash.SHA3(data), nil
	})
	if err != nil {
		return err
	}
	result, err := deck.Parse(output)
	if err != nil {
		return err
	}
	if !slices.Equal(result.S, canonical.S) {
		return expectf("rewrite did not use full canonical construction at requested k=%d", k)
	}
	wantSizes := []int{8, 8, 2}
	if mode == "replace" {
		wantSizes = []int{8, 8, 1}
		if k == 2 {
			wantSizes = []int{16, 1}
		}
	}
	if mode == "final" {
		wantSizes = nil
		if len(result.F) != 3 || len(result.S) != 0 {
			return expectf("final-small-group merge did not flatten to exactly three F cards")
		}
	}
	var leafSizes []int
	for _, want := range canonical.Subs {
		leaf, err := deck.Parse(want)
		if err != nil {
			return err
		}
		leafSizes = append(leafSizes, len(leaf.F))
		uuid := hash.SHA3(want)
		rid, held := content.AvailableByUUID(f.sparse.DB(), uuid)
		if !held {
			return expectf("canonical child %s absent", uuid)
		}
		got, err := content.Expand(f.sparse.DB(), rid)
		if err != nil || !bytes.Equal(got, want) {
			return expectf("canonical child bytes differ for %s: %v", uuid, err)
		}
	}
	if !slices.Equal(leafSizes, wantSizes) {
		return expectf("canonical leaf sizes %v, want literal A.6 sizes %v", leafSizes, wantSizes)
	}
	return nil
}