From 9300033e95f75f9a081e37b0cab8548248ef0ab7 Mon Sep 17 00:00:00 2001 From: vickydotbat Date: Fri, 10 Apr 2026 12:32:27 +0200 Subject: [PATCH] Fix Lockfile Schematics --- internal/topdata/native.go | 109 ++++++++++++++++++++++++++++++- internal/topdata/topdata_test.go | 23 +++++++ 2 files changed, 130 insertions(+), 2 deletions(-) diff --git a/internal/topdata/native.go b/internal/topdata/native.go index 71f2d43..32fdbae 100644 --- a/internal/topdata/native.go +++ b/internal/topdata/native.go @@ -1,6 +1,7 @@ package topdata import ( + "bytes" "encoding/json" "errors" "fmt" @@ -3600,17 +3601,121 @@ func saveLockfile(path string, lockData map[string]int) error { if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { return fmt.Errorf("create lockfile parent: %w", err) } - raw, err := json.MarshalIndent(lockData, "", " ") + keys, err := orderedLockfileKeys(path, lockData) + if err != nil { + return fmt.Errorf("read lockfile order: %w", err) + } + raw, err := marshalOrderedLockfile(keys, lockData) if err != nil { return fmt.Errorf("marshal lockfile: %w", err) } - raw = append(raw, '\n') if err := os.WriteFile(path, raw, 0o644); err != nil { return fmt.Errorf("write lockfile %s: %w", path, err) } return nil } +func orderedLockfileKeys(path string, lockData map[string]int) ([]string, error) { + existingKeys, err := readLockfileKeyOrder(path) + if err != nil && !errors.Is(err, os.ErrNotExist) { + return nil, err + } + + keys := make([]string, 0, len(lockData)) + seen := make(map[string]struct{}, len(lockData)) + for _, key := range existingKeys { + if _, ok := lockData[key]; !ok { + continue + } + keys = append(keys, key) + seen[key] = struct{}{} + } + + extraKeys := make([]string, 0, len(lockData)-len(keys)) + for key := range lockData { + if _, ok := seen[key]; ok { + continue + } + extraKeys = append(extraKeys, key) + } + slices.Sort(extraKeys) + keys = append(keys, extraKeys...) + return keys, nil +} + +func readLockfileKeyOrder(path string) ([]string, error) { + raw, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + decoder := json.NewDecoder(bytes.NewReader(raw)) + token, err := decoder.Token() + if err != nil { + return nil, err + } + delim, ok := token.(json.Delim) + if !ok || delim != '{' { + return nil, fmt.Errorf("%s must contain a JSON object", path) + } + + keys := []string{} + for decoder.More() { + token, err := decoder.Token() + if err != nil { + return nil, err + } + key, ok := token.(string) + if !ok { + return nil, fmt.Errorf("%s contains a non-string object key", path) + } + var value json.RawMessage + if err := decoder.Decode(&value); err != nil { + return nil, err + } + keys = append(keys, key) + } + + token, err = decoder.Token() + if err != nil { + return nil, err + } + delim, ok = token.(json.Delim) + if !ok || delim != '}' { + return nil, fmt.Errorf("%s must contain a JSON object", path) + } + return keys, nil +} + +func marshalOrderedLockfile(keys []string, lockData map[string]int) ([]byte, error) { + if len(keys) == 0 { + return []byte("{}\n"), nil + } + + var buffer bytes.Buffer + buffer.WriteString("{\n") + for index, key := range keys { + keyRaw, err := json.Marshal(key) + if err != nil { + return nil, err + } + valueRaw, err := json.Marshal(lockData[key]) + if err != nil { + return nil, err + } + buffer.WriteString(" ") + buffer.Write(keyRaw) + buffer.WriteString(": ") + buffer.Write(valueRaw) + if index < len(keys)-1 { + buffer.WriteByte(',') + } + buffer.WriteByte('\n') + } + buffer.WriteString("}\n") + return buffer.Bytes(), nil +} + func loadLegacyTLK(root string) (*legacyTLKData, error) { info, err := os.Stat(root) if err != nil { diff --git a/internal/topdata/topdata_test.go b/internal/topdata/topdata_test.go index 01230da..8706b6d 100644 --- a/internal/topdata/topdata_test.go +++ b/internal/topdata/topdata_test.go @@ -108,6 +108,29 @@ func TestResolvedTableRegistryRegistersConsistentTables(t *testing.T) { } } +func TestSaveLockfilePreservesExistingOrderAndAppendsNewKeys(t *testing.T) { + root := t.TempDir() + lockPath := filepath.Join(root, "lock.json") + writeFile(t, lockPath, "{\n \"b\": 2,\n \"a\": 1\n}\n") + + if err := saveLockfile(lockPath, map[string]int{ + "b": 2, + "a": 1, + "c": 3, + }); err != nil { + t.Fatalf("saveLockfile failed: %v", err) + } + + got, err := os.ReadFile(lockPath) + if err != nil { + t.Fatalf("read lockfile: %v", err) + } + want := "{\n \"b\": 2,\n \"a\": 1,\n \"c\": 3\n}\n" + if string(got) != want { + t.Fatalf("unexpected lockfile contents:\n%s", string(got)) + } +} + func TestResolvedTableRegistryRejectsDuplicateKeys(t *testing.T) { _, err := newResolvedTableRegistry([]nativeCollectedDataset{ {