package app import ( "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "net/netip" "sort" "strings" ) type transportOwnerLockClearFilter struct { ClientID string DestinationIPs []string } func normalizeTransportOwnerLockClearRequest(in TransportOwnerLocksClearRequest) (transportOwnerLockClearFilter, error) { filter := transportOwnerLockClearFilter{ ClientID: sanitizeID(in.ClientID), } seen := map[string]struct{}{} appendIP := func(raw string) error { v := strings.TrimSpace(raw) if v == "" { return nil } addr, err := netip.ParseAddr(v) if err != nil || !addr.Is4() { return fmt.Errorf("invalid destination ip: %q", raw) } key := addr.String() if _, ok := seen[key]; ok { return nil } seen[key] = struct{}{} filter.DestinationIPs = append(filter.DestinationIPs, key) return nil } if err := appendIP(in.DestinationIP); err != nil { return transportOwnerLockClearFilter{}, err } for _, raw := range in.DestinationIPs { if err := appendIP(raw); err != nil { return transportOwnerLockClearFilter{}, err } } sort.Strings(filter.DestinationIPs) if filter.ClientID == "" && len(filter.DestinationIPs) == 0 { return transportOwnerLockClearFilter{}, fmt.Errorf("at least one selector is required: client_id or destination_ip(s)") } return filter, nil } func splitTransportOwnerLocksByFilter(items []TransportOwnerLockRecord, filter transportOwnerLockClearFilter) (matched, remaining []TransportOwnerLockRecord) { matchIPs := map[string]struct{}{} for _, ip := range filter.DestinationIPs { matchIPs[ip] = struct{}{} } matched = make([]TransportOwnerLockRecord, 0, len(items)) remaining = make([]TransportOwnerLockRecord, 0, len(items)) for _, it := range items { match := true if filter.ClientID != "" && sanitizeID(it.ClientID) != filter.ClientID { match = false } if len(matchIPs) > 0 { dst := strings.TrimSpace(it.DestinationIP) if addr, err := netip.ParseAddr(dst); err == nil && addr.Is4() { dst = addr.String() } if _, ok := matchIPs[dst]; !ok { match = false } } if match { matched = append(matched, it) } else { remaining = append(remaining, it) } } return matched, remaining } func digestTransportOwnerLocksClear(baseRevision int64, filter transportOwnerLockClearFilter, matched []TransportOwnerLockRecord) string { keys := make([]string, 0, len(matched)) for _, it := range matched { keys = append(keys, strings.TrimSpace(it.ClientID)+"|"+strings.TrimSpace(it.DestinationIP)) } sort.Strings(keys) payload := struct { BaseRevision int64 `json:"base_revision"` ClientID string `json:"client_id,omitempty"` DestinationIPs []string `json:"destination_ips,omitempty"` Matches []string `json:"matches,omitempty"` }{ BaseRevision: baseRevision, ClientID: filter.ClientID, DestinationIPs: append([]string(nil), filter.DestinationIPs...), Matches: keys, } b, _ := json.Marshal(payload) h := sha256.Sum256(b) return hex.EncodeToString(h[:]) }