fix(ipcohort): never return nil Cohort, zero value is empty set

All constructors (LoadFile, LoadFiles, ReadAll) now return &Cohort{}
on error instead of nil. Callers can use the result directly without
nil checks — an empty Cohort is a valid empty set that returns false
for all Contains calls.
This commit is contained in:
AJ ONeal 2026-05-04 14:28:53 -06:00
parent 1dacbdd8f7
commit 051a244083
No known key found for this signature in database
2 changed files with 31 additions and 15 deletions

View File

@ -158,13 +158,13 @@ func ParseIPv4(raw string) (ipv4net IPv4Net, err error) {
), nil
}
// LoadFile reads path and parses it into a Cohort. On open or read
// errors, returns nil + error. On parse errors only, returns the partial
// cohort + a joined error.
// LoadFile reads path and parses it into a Cohort. Always returns a
// non-nil Cohort (empty on error) so callers can use the zero value
// as an empty set without nil checks.
func LoadFile(path string) (*Cohort, error) {
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("could not load %q: %w", path, err)
return &Cohort{}, fmt.Errorf("could not load %q: %w", path, err)
}
defer func() { _ = f.Close() }()
return ParseCSV(f)
@ -173,21 +173,17 @@ func LoadFile(path string) (*Cohort, error) {
// LoadFiles loads and merges multiple files into one Cohort. Useful when
// hosts and networks are stored in separate files.
//
// On open or read errors, returns nil + error. On parse errors only,
// returns the partial cohort + a joined error so callers can choose to
// proceed with what loaded.
// Always returns a non-nil Cohort. On errors, the cohort contains
// whatever loaded successfully; the caller decides whether to proceed.
func LoadFiles(paths ...string) (*Cohort, error) {
var hosts []uint32
var nets []IPv4Net
var parseErrs []error
var errs []error
for _, path := range paths {
c, err := LoadFile(path)
if c == nil {
return nil, err
}
if err != nil {
parseErrs = append(parseErrs, fmt.Errorf("%s: %w", path, err))
errs = append(errs, fmt.Errorf("%s: %w", path, err))
}
hosts = append(hosts, c.hosts...)
nets = append(nets, c.nets...)
@ -197,8 +193,8 @@ func LoadFiles(paths ...string) (*Cohort, error) {
sortNets(nets)
c := &Cohort{hosts: hosts, nets: nets}
if len(parseErrs) > 0 {
return c, errors.Join(parseErrs...)
if len(errs) > 0 {
return c, errors.Join(errs...)
}
return c, nil
}
@ -225,7 +221,8 @@ func ReadAll(r *csv.Reader) (*Cohort, error) {
break
}
if err != nil {
return nil, fmt.Errorf("csv read error: %w", err)
c := &Cohort{hosts: hosts, nets: nets}
return c, fmt.Errorf("csv read error: %w", err)
}
if len(record) == 0 {

View File

@ -0,0 +1,19 @@
package ipcohort
import (
"net/http"
"strings"
)
// HTTP middleware example
func (c *Cohort) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Get client IP (basic; improve with X-Forwarded-For parsing if behind proxy)
ip, _, _ := strings.Cut(r.RemoteAddr, ":")
if blocked, _ := c.Contains(ip); blocked {
http.Error(w, "Access denied", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}