package geodata import "strings" // Categories returns the database's categories, filtered by a case-insensitive // substring of the category code. A non-positive limit returns all of them: // the category index is small even for the largest databases, and the panel // filters it client-side so typing in the search box costs no requests. func (s *Store) Categories(name, query string, offset, limit int) (GeoCategoryPage, error) { idx, err := s.index(name) if err != nil { return GeoCategoryPage{}, err } matched := idx.categories if query = strings.ToLower(strings.TrimSpace(query)); query != "" { matched = make([]GeoCategory, 0, len(idx.categories)) for _, category := range idx.categories { if strings.Contains(category.Code, query) { matched = append(matched, category) } } } page := GeoCategoryPage{Total: len(matched), Items: []GeoCategory{}} from, to := categoryBounds(len(matched), offset, limit) page.Items = append(page.Items, matched[from:to]...) return page, nil } // Entries returns one page of a category's rules, filtered by a // case-insensitive substring of the rule value. The page is scanned out of the // file on each call: a category such as geosite's category-ads-all holds well // over a hundred thousand rules, and holding those in memory to serve one // screenful of them is what the panel cannot afford. func (s *Store) Entries(name, code, query string, offset, limit int) (GeoEntryPage, error) { idx, err := s.index(name) if err != nil { return GeoEntryPage{}, err } code = strings.ToLower(strings.TrimSpace(code)) if _, ok := idx.byCode[code]; !ok { return GeoEntryPage{}, ErrUnknownCategory } if _, err := s.resolve(name); err != nil { return GeoEntryPage{}, err } if offset < 0 { offset = 0 } if limit <= 0 || limit > MaxPageSize { limit = MaxPageSize } spans := idx.spans[code] if len(spans) == 0 { return GeoEntryPage{}, ErrUnknownCategory } s.scan.Lock() defer s.scan.Unlock() records, err := s.recordsLocked(name, code, spans) if err != nil { return GeoEntryPage{}, err } return scanEntries(records, idx.kind, code, strings.ToLower(strings.TrimSpace(query)), offset, limit) } // Lookup reports whether a category exists in the database, without paying for // the entry data. The code is matched verbatim apart from case: this backs the // routing-token validator, and the core does not forgive a stray space either, // so trimming one here would hide the very typo the validator exists to find. func (s *Store) Lookup(name, code string) (GeoCategory, error) { idx, err := s.index(name) if err != nil { return GeoCategory{}, err } category, ok := idx.byCode[strings.ToLower(code)] if !ok { return GeoCategory{}, ErrUnknownCategory } return category, nil } func categoryBounds(total, offset, limit int) (int, int) { if limit <= 0 { if offset < 0 { offset = 0 } if offset > total { offset = total } return offset, total } return sliceBounds(total, offset, limit) } func sliceBounds(total, offset, limit int) (int, int) { if offset < 0 { offset = 0 } if offset > total { offset = total } if limit <= 0 || limit > MaxPageSize { limit = MaxPageSize } to := offset + limit if to > total { to = total } return offset, to } func (s *Store) recordsLocked(name, code string, spans []byteSpan) ([][]byte, error) { info, err := s.resolve(name) if err != nil { return nil, err } key := fileKey{name: name, size: info.Size(), modTime: info.ModTime().UnixNano()} if s.hot.key == key && s.hot.code == code { return s.hot.records, nil } records, err := readSpans(s.dir, name, spans) if err != nil { return nil, err } s.hot = hotRecord{key: key, code: code, records: records} return records, nil }