// Package asinfo provides information about Autonomous Systems (AS) package asinfo import ( "bytes" "compress/gzip" _ "embed" "encoding/json" "io" "sync" ) //go:embed asdata.json.gz var asDataGZ []byte // Info represents information about an Autonomous System type Info struct { ASN int `json:"asn"` Handle string `json:"handle"` Description string `json:"description"` } // Registry provides access to AS information type Registry struct { mu sync.RWMutex byASN map[int]*Info } var ( //nolint:gochecknoglobals // Singleton pattern for embedded data defaultRegistry *Registry //nolint:gochecknoglobals // Singleton pattern for embedded data once sync.Once ) // initRegistry initializes the default registry with embedded data func initRegistry() { defaultRegistry = &Registry{ byASN: make(map[int]*Info), } // Decompress the gzipped data gzReader, err := gzip.NewReader(bytes.NewReader(asDataGZ)) if err != nil { panic("failed to create gzip reader: " + err.Error()) } defer func() { _ = gzReader.Close() }() jsonData, err := io.ReadAll(gzReader) if err != nil { panic("failed to decompress AS data: " + err.Error()) } var asInfos []Info if err := json.Unmarshal(jsonData, &asInfos); err != nil { panic("failed to unmarshal embedded AS data: " + err.Error()) } for i := range asInfos { info := &asInfos[i] defaultRegistry.byASN[info.ASN] = info } } // Get returns AS information for the given ASN func Get(asn int) (*Info, bool) { once.Do(initRegistry) defaultRegistry.mu.RLock() defer defaultRegistry.mu.RUnlock() info, ok := defaultRegistry.byASN[asn] if !ok { return nil, false } // Return a copy to prevent mutation infoCopy := *info return &infoCopy, true } // GetDescription returns just the description for an ASN func GetDescription(asn int) string { info, ok := Get(asn) if !ok { return "" } return info.Description } // GetHandle returns just the handle for an ASN func GetHandle(asn int) string { info, ok := Get(asn) if !ok { return "" } return info.Handle } // Total returns the total number of AS entries in the registry func Total() int { once.Do(initRegistry) defaultRegistry.mu.RLock() defer defaultRegistry.mu.RUnlock() return len(defaultRegistry.byASN) } // All returns all AS information as a slice // Note: This creates a copy of all data, use sparingly func All() []Info { once.Do(initRegistry) defaultRegistry.mu.RLock() defer defaultRegistry.mu.RUnlock() result := make([]Info, 0, len(defaultRegistry.byASN)) for _, info := range defaultRegistry.byASN { result = append(result, *info) } return result } // Search searches for AS entries where the handle or description contains the query // This is a simple case-sensitive substring search func Search(query string) []Info { once.Do(initRegistry) defaultRegistry.mu.RLock() defer defaultRegistry.mu.RUnlock() var results []Info for _, info := range defaultRegistry.byASN { if contains(info.Handle, query) || contains(info.Description, query) { results = append(results, *info) } } return results } // contains is a simple substring search func contains(s, substr string) bool { return len(substr) > 0 && len(s) >= len(substr) && containsImpl(s, substr) } func containsImpl(s, substr string) bool { for i := 0; i <= len(s)-len(substr); i++ { if s[i:i+len(substr)] == substr { return true } } return false }