package park import ( "context" "errors" "path" "sort" "strings" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/release" ) var ( ErrLocalIdentity = errors.New("park: local retirement identity mismatch") ErrLocalInventory = errors.New("park: missing or unsafe inventory") ErrLocalJournal = errors.New("park: invalid or conflicting local journal") ErrLocalUnsupported = errors.New("park: local retirement unsupported") errLocalComplete = errors.New("park: local obligation already completed") ) type Local struct { Authorize func(context.Context, *hostclaim.Claim, hostclaim.Obligation) error Guard hostguard.Guard Claims release.Store DataDir, Box string afterRename func(string) error } type localItem struct { Path string Stamp uint64 Dev, Ino uint64 Type uint32 Missing, Done bool } type localJournal struct { V int Host, Incarnation, Op, Obligation, Retirement, Mode string Installation, ArtifactStamp uint64 Items []localItem } func localComponent(s string) bool { return s != "" && s != "." && s != ".." && !strings.ContainsAny(s, "/\\\x00") } func localPath(s string) bool { if s == "" || path.IsAbs(s) || path.Clean(s) != s || strings.ContainsAny(s, "\\\x00") { return false } for _, p := range strings.Split(s, "/") { if !localComponent(p) { return false } } return s != "hostguard" && !strings.HasPrefix(s, "hostguard/") && s != ".parked" && !strings.HasPrefix(s, ".parked/") } func (l Local) chore(c *hostclaim.Claim, captured hostguard.Captured, id, op string) (hostclaim.Obligation, error) { if c == nil { return hostclaim.Obligation{}, ErrLocalIdentity } var found *hostclaim.Obligation for i := range c.Obligations { if c.Obligations[i].ID == id && c.Obligations[i].Op == op { found = &c.Obligations[i] break } } if found == nil { return hostclaim.Obligation{}, errLocalComplete } if c.Holder == nil || c.Holder.Incarnation != captured.Incarnation || found.Kind != "park-local" || found.Box != l.Box || found.Owner != "box:"+l.Box || !localComponent(found.Retirement) { return hostclaim.Obligation{}, ErrLocalIdentity } if _, ok := c.Inventory[l.Box]; !ok { return hostclaim.Obligation{}, ErrLocalInventory } return *found, nil } func (l Local) RetireArtifacts(ctx context.Context, host string, captured hostguard.Captured, id, op string, stamp uint64) error { if stamp == 0 { return ErrLocalIdentity } return l.retire(ctx, host, captured, id, op, "artifacts", stamp) } // RetireSuperseded is retire-artifacts(S) of claim §6.4 for an admitted // writer's own superseded local generation (claim Plan 9 D4): a restarted // landing probe, or a cold wake after the open, over its predecessor's // database. No chore is read, required or completed. The authority is the // guard at the active installation captured, newer than stamp, together with // check, which runs on every fresh claim under the guard and again in the // inventory CAS. Exactly paths are parked, each of which the inventory must // tag stamp; the same CAS tags them captured.Stamp, the ownership the // successor records before it materializes them (§6.6 P6-D2), so its own // successor can retire them in turn. No stamp moves and every other path and // box is left as found. A replay of the same retirement resumes its journal. func (l Local) RetireSuperseded(ctx context.Context, host string, captured hostguard.Captured, op, retirement string, stamp uint64, paths []string, check func(*hostclaim.Claim) error) (before, after hostclaim.Observed, resultErr error) { if !localSupported() { return before, after, ErrLocalUnsupported } if ctx == nil || check == nil || l.Claims == nil || l.Box == "" || l.DataDir == "" || l.Guard.DataDir != l.DataDir || captured.Incarnation == "" || captured.Stamp == 0 || stamp == 0 || stamp >= captured.Stamp || !localComponent(op) || !localComponent(retirement) || len(paths) == 0 { return before, after, ErrLocalIdentity } sorted := append([]string(nil), paths...) sort.Strings(sorted) for i := range sorted { if !localPath(sorted[i]) { return before, after, ErrLocalInventory } for k := i + 1; k < len(sorted); k++ { if sorted[k] == sorted[i] || strings.HasPrefix(sorted[k], sorted[i]+"/") { return before, after, ErrLocalInventory } } } selected := func(c *hostclaim.Claim) error { if c == nil || c.Holder == nil || c.Holder.Incarnation != captured.Incarnation || c.Holder.Stamp != captured.Stamp { return ErrLocalIdentity } return check(c) } var j localJournal guarded := func(c *hostclaim.Claim) error { if err := selected(c); err != nil { return err } entry, ok := c.Inventory[l.Box] if !ok { return ErrLocalInventory } superseded, successor := 0, 0 for _, p := range sorted { switch entry.Paths[p] { case stamp: superseded++ case captured.Stamp: successor++ default: return ErrLocalInventory } } if successor == len(sorted) { return errLocalComplete } if superseded != len(sorted) { return ErrLocalInventory } j = localJournal{V: 1, Host: host, Incarnation: captured.Incarnation, Op: op, Retirement: retirement, Mode: "superseded", Installation: captured.Stamp, ArtifactStamp: stamp} for _, p := range sorted { j.Items = append(j.Items, localItem{Path: p, Stamp: stamp}) } return nil } work := func() error { if err := l.parkLocal(ctx, &j); err != nil { return err } fresh, err := hostclaim.Read(ctx, l.Claims, host) if err != nil { return err } if fresh.Absent { return ErrLocalIdentity } before = fresh after, err = hostclaim.Update(ctx, l.Claims, fresh, func(c *hostclaim.Claim) error { if err := selected(c); err != nil { return err } entry, ok := c.Inventory[l.Box] if !ok { return ErrLocalInventory } for _, item := range j.Items { if entry.Paths[item.Path] != item.Stamp { return ErrLocalInventory } } for _, item := range j.Items { entry.Paths[item.Path] = captured.Stamp } c.Inventory[l.Box] = entry return nil }) return err } err := l.Guard.With(ctx, host, captured, guarded, work) if errors.Is(err, errLocalComplete) { return before, after, nil } return before, after, err } func (l Local) RetireInstallation(ctx context.Context, host string, captured hostguard.Captured, id, op string) error { return l.retire(ctx, host, captured, id, op, "installation", 0) } func (l Local) retire(ctx context.Context, host string, captured hostguard.Captured, id, op, mode string, stamp uint64) error { if !localSupported() { return ErrLocalUnsupported } if ctx == nil || l.Authorize == nil || l.Claims == nil || l.Box == "" || l.DataDir == "" || l.Guard.DataDir != l.DataDir || (captured.Stamp == 0 && mode != "empty") || id == "" || op == "" { return ErrLocalIdentity } obs, err := hostclaim.Read(ctx, l.Claims, host) if err != nil { return err } if obs.Absent { return ErrLocalIdentity } if _, err := l.chore(obs.Claim, captured, id, op); err != nil { if errors.Is(err, errLocalComplete) { return nil } return err } var j localJournal var inventory map[string]uint64 check := func(c *hostclaim.Claim) error { o, err := l.chore(c, captured, id, op) if err != nil { return err } if err := l.Authorize(ctx, c, o); err != nil { return err } inventory = c.Inventory[l.Box].Paths if mode == "empty" && len(inventory) != 0 { return ErrLocalInventory } paths := make([]string, 0, len(inventory)) for p, s := range inventory { if !localPath(p) || s == 0 { return ErrLocalInventory } paths = append(paths, p) } sort.Strings(paths) for i := range paths { for k := i + 1; k < len(paths); k++ { if strings.HasPrefix(paths[k], paths[i]+"/") { return ErrLocalInventory } } } j = localJournal{V: 1, Host: host, Incarnation: captured.Incarnation, Op: op, Obligation: id, Retirement: o.Retirement, Mode: mode, Installation: captured.Stamp, ArtifactStamp: stamp} for _, p := range paths { if mode == "installation" || inventory[p] == stamp { j.Items = append(j.Items, localItem{Path: p, Stamp: inventory[p]}) } } return nil } work := func() error { if err := l.parkLocal(ctx, &j); err != nil { return err } fresh, err := hostclaim.Read(ctx, l.Claims, host) if err != nil { return err } if fresh.Absent { return ErrLocalIdentity } _, err = hostclaim.Update(ctx, l.Claims, fresh, func(c *hostclaim.Claim) error { o, err := l.chore(c, captured, id, op) if err != nil { return err } if err := l.Authorize(ctx, c, o); err != nil { return err } entry := c.Inventory[l.Box] for _, item := range j.Items { if entry.Paths[item.Path] != item.Stamp { return ErrLocalInventory } } if (mode == "installation" || mode == "empty") && len(entry.Paths) != len(j.Items) { return ErrLocalInventory } for _, item := range j.Items { delete(entry.Paths, item.Path) } c.Inventory[l.Box] = entry return hostclaim.CompleteObligation(op, id)(c) }) return err } if mode == "installation" { err = l.Guard.RetireInstallationWith(ctx, host, captured, check, work) } else if mode == "empty" { err = l.Guard.WithNoInstallation(ctx, host, captured.Incarnation, check, work) } else { err = l.Guard.With(ctx, host, captured, check, work) } if errors.Is(err, errLocalComplete) { return nil } return err } func (l Local) RecordInventory(ctx context.Context, host string, captured hostguard.Captured, layout string, paths []string, check func(*hostclaim.Claim) error) error { if !localSupported() { return ErrLocalUnsupported } if ctx == nil || l.Claims == nil || l.Box == "" || l.Guard.DataDir != l.DataDir || l.DataDir == "" || check == nil || captured.Stamp == 0 || (layout != "flat" && layout != "nested") { return ErrLocalInventory } for _, p := range paths { if !localPath(p) { return ErrLocalInventory } } return l.Guard.With(ctx, host, captured, check, func() error { obs, err := hostclaim.Read(ctx, l.Claims, host) if err != nil { return err } if obs.Absent { return ErrLocalIdentity } _, err = hostclaim.Update(ctx, l.Claims, obs, func(c *hostclaim.Claim) error { if c.Holder == nil || c.Holder.Incarnation != captured.Incarnation { return ErrLocalIdentity } if err := check(c); err != nil { return err } if c.Inventory == nil { c.Inventory = make(map[string]hostclaim.InventoryEntry) } entry, exists := c.Inventory[l.Box] if exists && entry.Layout != layout && len(entry.Paths) != 0 { return ErrLocalInventory } entry.Layout = layout if entry.Paths == nil { entry.Paths = make(map[string]uint64) } for _, p := range paths { if old := entry.Paths[p]; old != 0 && old != captured.Stamp { return ErrLocalInventory } entry.Paths[p] = captured.Stamp } all := make([]string, 0, len(entry.Paths)) for p, s := range entry.Paths { if !localPath(p) || s == 0 { return ErrLocalInventory } all = append(all, p) } sort.Strings(all) for i := range all { for k := i + 1; k < len(all); k++ { if strings.HasPrefix(all[k], all[i]+"/") { return ErrLocalInventory } } } c.Inventory[l.Box] = entry return nil }) return err }) } func (l Local) AcknowledgeEmpty(ctx context.Context, host, incarnation, id, op string) error { return l.retire(ctx, host, hostguard.Captured{Incarnation: incarnation}, id, op, "empty", 0) }